Package: uactor
uActor: Process Actor ModeluActor is a process actor library for Python with a simple yet powerful API,
implementing theactor modelatopmultiprocessing,
with no dependencies other than thePython Standard Library.Simple: Minimalistic API, no boilerplate required.Flexible: Trivial to integrate, meant to be extended.Concurrent: Share workload over CPU cores, and across the network.Documentation:uactor.readthedocs.ioUsage:importosimportuactorclassActor(uactor.Actor):defhello(self):returnf'Hello from subprocess{os.getpid()}!'print(f'Hello from process{os.getpid()}!')# Hello from process 22682!print(Actor().hello())# Hello from subprocess 22683!QuickstartInstallationYou can install it usingpip.pipinstalluactorOr alternatively by including our singleuactor.pyfile into your project.Your first actorWith uActor, actors are defined as classes inheriting fromuactor.Actor,
with some special attributes we'll cover later.importuactorclassMyActor(uactor.Actor):defmy_method(self):returnTrueDuring instantiation, every actor is initialized on its own dedicated
process, returning a proxy.my_actor_proxy=MyActor()my_actor_proxy.my_method()Once you're done with your actor, it is always a good idea to finalize its
process withuactor.Actor.shutdownmethod.my_actor_proxy.shutdown()Alternatively,uactor.Actorinstances can be used as context managers, so
the actor process will be finalized once we're done with it.withMyActor()asmy_actor_proxy:my_actor_proxy.my_method()Actor processes will be also finished when every proxy gets garbage-collected
on its parent process.Returning result proxiesActor methods can return proxies instead of actual objects, keeping them
sound and safe on our actor process.To specify which proxy will be returned from an specific method, we can add
both method name and proxy typeid touactor.Actor._method_to_typeid_special
class attribute.importuactorclassMyActor(uactor.Actor):_method_to_typeid_={'my_method':'dict'}def__init__(self):self.my_data={}defmy_method(self):returnself.my_dataOr, alternatively, we can explicitly create a proxy for our object, usinguactor.proxyutility function.importuactorclassMyActor(uactor.Actor):def__init__(self):self.my_data={}defmy_method(self):returnuactor.proxy(self.my_data,'dict')There is a limitation with proxies, applying two different proxies to
the same object will raise an exception, this is likely to change in
the future.Becoming asynchronous (and concurrent)Actor methods are fully synchronous by default, which is usually not very
useful on distributed software, the following example will show you how
to return asynchronous results from the actor.importtimeimportmultiprocessing.poolimportuactorclassMyActor(uactor.Actor):_method_to_typeid_={'my_method':'AsyncResult'}def__init__(self):self.threadpool=multiprocessing.pool.ThreadPool()defmy_method(self):returnself.threadpool.apply_async(time.sleep,[10])# wait 10swithMyActor()asmy_actor:# will return immediatelyresult=my_actor.my_method()# will take 10 secondsresult.wait()Based on this, we can now run code concurrently running on the same actor.withMyActor()asmy_actor:# these will return immediatelyresult_a=my_actor.my_method()result_b=my_actor.my_method()# these all will take 10 seconds in totalresult_a.wait()result_b.wait()And now we can to parallelize workloads across different actor processes.actor_a=MyActor()actor_b=MyActor()withactor_a,actor_b:# these both will return immediatelyresult_a=actor_a.my_method()result_b=actor_b.my_method()result_a.wait()# this will take ~10s to completeresult_b.wait()# this will be immediate (we already waited 10s)Next stepsYou can dive into ourdocumentationor
take a look at our code examples.The basics:Actor inheritance.Actor lifetime.Result proxies.Method callbacks.Advanced patterns:Sticky processes.Actor pool.Networking.uActor designWith the constant rise in CPU core count, highly threaded python applications
are still pretty rare (except for distributed processing frameworks likecelery), this is due a few reasons:threadingcannot use multiple cores becausePython Global Interpreter Lockforces the interpreter to run on a
single core.multiprocessing, meant to overcome threading limitations
by using processes, exposes a pretty convoluted API as processes
are way more complex, exposing many quirks and limitations.uActor allows implementing distributed software as easy as just declaring
and instancing classes, following theactor model, by thinly
wrapping the standardSyncManagerto circumvent most omultiprocessingcomplexity and some of its flaws.uActor API is designed to be both minimalistic and intuitive, but still few
compromises had to be taken to leverage onSyncManageras much as possible, as it is both somewhat actively maintained and
already available as part of thePython Standard Library.ActorsJust like the actor programming model revolves around the actor entity,
uActor features theuactor.Actorbase class.When an actor class is declared, by inheriting fromuactor.Actor, itsActor.proxy_classgets also inherited and updated to mirror the actor
interface, either following the explicit list of properties defined atActor._exposed_or implicitly by actor public methods.Actor.manager_classis also inherited registering actor specific proxies
defined inActor._proxies_mapping (key used as a typeid) along with'actor'and'auto'special proxies.Keep in mind the defaultActor.manager_class,uactor.ActorManager, already
includes every proxy fromSyncManager(including the internalAsyncResultandIterator) which are all available to the actor and ready
use (you can callActor.manager_class.typeids()to list them all).As a reference, these are all the availableuactor.Actorconfiguration
class attributes:manager_class: manager base class (defaults to parent's one, up touactor.ActorManager).proxy_class: actor proxy class (defaults to parent's one, up touactor.ActorProxy)._options_: option mapping will be passed tomanager_class._exposed_: list of explicitly exposed methods will be made available byproxy_class, ifNoneor undefined then all public methods will be
exposed._proxies_: mapping (typeid, proxy class) of additional proxies will be
registered in themanager_classand, thus, will be available to
be returned by the actor._method_to_typeid_: mapping (method name, typeid) defining which method
return values will be wrapped into proxies when invoked fromproxy_class.When anuactor.Actorclass is instantiated, a new process is spawned and auactor.Actor.proxy_classinstance is returned (as the real actor will be
kept safe in said process), transparently exposing a message-based interface.importosimportuactorclassActor(uactor.Actor):defgetpid(self):returnos.getpid()actor=Actor()print('My process id is',os.getpid())# My process id is 153333print('Actor process id is ',actor.getpid())# Actor process id is 153344ProxiesProxies are objects communicating with the actor process, exposing
a similar interface, in the most transparent way possible.It is implied most calls made to a proxy will result on inter-process
communication and serialization overhead.To alleviate the serialization cost, actor methods can also return proxies,
so the real data is kept well inside the actor process boundaries, which can
be efficiently shared between processes with very little serialization cost.Actors can define which proxy will be used to expose the result of certain
methods by defining that in theirActor._method_to_typeid_property.importuactorclassActor(uactor.Actor):_method_to_typeid_={'get_mapping':'dict'}...defget_data(self):returnself.my_data_dictOr, alternatively, using theuactor.proxyfunction, receiving both value
and a proxytypeid(as inSyncManagersemantics).importuactorclassActor(uactor.Actor):...defget_data(self):returnuactor.proxy(self.my_data_dict,'dict')Keep in minduactor.proxycan only be called from inside the actor process
(it will raiseuactor.ProxyErrorotherwise), as proxies can only be created
from there.You can define your own proxy classes (followingBaseProxysemantics), and they will be made available in an actor by including it on
theActor._proxies_mapping (along its typeid).importuactorclassMyDataProxy(uactor.BaseProxy):defmy_method(self):returnself._callmethod('my_method')my_other_method=uactor.ProxyMethod('my_other_method')classActor(uactor.Actor):_proxies_={'MyDataProxy':MyDataProxy}_method_to_typeid_={'get_data':'MyDataProxy'}...In addition to all proxies imported from bothSyncManager(including internal ones asIteratorandAsyncResult) andActor._proxies_, we always register these ones:actor: proxy to the current process actor.auto: dynamic proxy based based on the wrapped object.You can list all available proxies (which can vary between python versions)
by callingActorManager.typeids():importuactorprint(uactor.Actor.manager_class.typeids())# ('Queue', 'JoinableQueue', 'Event', ..., 'auto', 'actor')print(uactor.ActorManager.typeids())# ('Queue', 'JoinableQueue', 'Event', ..., 'auto')ContributinguActor is deliberately very small in scope, while still aiming to be easily
extended, so extra functionality might be implemented via external means.If you find any bug or a possible improvement to existing functionality it
will likely be accepted so feel free to contribute.If, in the other hand, you feel a feature is missing, you can either create
another library using uActor as dependency or fork this project.LicenseCopyright (c) 2020, Felipe A Hernandez.MIT License (seeLICENSE).