forked from goodboy/tractor
Reorg streaming section
parent
4ee35038fb
commit
2f773fc883
269
README.rst
269
README.rst
|
@ -280,8 +280,60 @@ to all others with ease over standard network protocols).
|
||||||
.. _Executor: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor
|
.. _Executor: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor
|
||||||
|
|
||||||
|
|
||||||
Async IPC using *portals*
|
Cancellation
|
||||||
*************************
|
************
|
||||||
|
``tractor`` supports ``trio``'s cancellation_ system verbatim.
|
||||||
|
Cancelling a nursery block cancels all actors spawned by it.
|
||||||
|
Eventually ``tractor`` plans to support different `supervision strategies`_ like ``erlang``.
|
||||||
|
|
||||||
|
.. _supervision strategies: http://erlang.org/doc/man/supervisor.html#sup_flags
|
||||||
|
|
||||||
|
|
||||||
|
Remote error propagation
|
||||||
|
************************
|
||||||
|
Any task invoked in a remote actor should ship any error(s) back to the calling
|
||||||
|
actor where it is raised and expected to be dealt with. This way remote actors
|
||||||
|
are never cancelled unless explicitly asked or there's a bug in ``tractor`` itself.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
async def assert_err():
|
||||||
|
assert 0
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with tractor.open_nursery() as n:
|
||||||
|
real_actors = []
|
||||||
|
for i in range(3):
|
||||||
|
real_actors.append(await n.start_actor(
|
||||||
|
f'actor_{i}',
|
||||||
|
rpc_module_paths=[__name__],
|
||||||
|
))
|
||||||
|
|
||||||
|
# start one actor that will fail immediately
|
||||||
|
await n.run_in_actor('extra', assert_err)
|
||||||
|
|
||||||
|
# should error here with a ``RemoteActorError`` containing
|
||||||
|
# an ``AssertionError`` and all the other actors have been cancelled
|
||||||
|
|
||||||
|
try:
|
||||||
|
# also raises
|
||||||
|
tractor.run(main)
|
||||||
|
except tractor.RemoteActorError:
|
||||||
|
print("Look Maa that actor failed hard, hehhh!")
|
||||||
|
|
||||||
|
|
||||||
|
You'll notice the nursery cancellation conducts a *one-cancels-all*
|
||||||
|
supervisory strategy `exactly like trio`_. The plan is to add more
|
||||||
|
`erlang strategies`_ in the near future by allowing nurseries to accept
|
||||||
|
a ``Supervisor`` type.
|
||||||
|
|
||||||
|
.. _exactly like trio: https://trio.readthedocs.io/en/latest/reference-core.html#cancellation-semantics
|
||||||
|
.. _erlang strategies: http://learnyousomeerlang.com/supervisors
|
||||||
|
|
||||||
|
|
||||||
|
IPC using *portals*
|
||||||
|
*******************
|
||||||
``tractor`` introduces the concept of a *portal* which is an API
|
``tractor`` introduces the concept of a *portal* which is an API
|
||||||
borrowed_ from ``trio``. A portal may seem similar to the idea of
|
borrowed_ from ``trio``. A portal may seem similar to the idea of
|
||||||
a RPC future_ except a *portal* allows invoking remote *async* functions and
|
a RPC future_ except a *portal* allows invoking remote *async* functions and
|
||||||
|
@ -305,10 +357,26 @@ channels_ system or shipping code over the network.
|
||||||
|
|
||||||
This *portal* approach turns out to be paricularly exciting with the
|
This *portal* approach turns out to be paricularly exciting with the
|
||||||
introduction of `asynchronous generators`_ in Python 3.6! It means that
|
introduction of `asynchronous generators`_ in Python 3.6! It means that
|
||||||
actors can compose nicely in a data processing pipeline.
|
actors can compose nicely in a data streaming pipeline.
|
||||||
|
|
||||||
As an example here's an actor that streams for 1 second from a remote async
|
|
||||||
generator function running in a separate actor:
|
Streaming
|
||||||
|
*********
|
||||||
|
By now you've figured out that ``tractor`` lets you spawn
|
||||||
|
process based actors that can invoke cross-process async functions
|
||||||
|
between each other and all with structured concurrency built in, but,
|
||||||
|
the **real power** is the ability to accomplish cross-process *streaming*.
|
||||||
|
|
||||||
|
|
||||||
|
Asynchronous generators
|
||||||
|
+++++++++++++++++++++++
|
||||||
|
The default streaming function is simply an async generator definition.
|
||||||
|
Every value *yielded* from the generator is delivered to the calling
|
||||||
|
portal exactly like if you had invoked the function in-process meaning
|
||||||
|
you can ``async for`` to receive each value on the calling side.
|
||||||
|
|
||||||
|
As an example here's a parent actor that streams for 1 second from a
|
||||||
|
spawned subactor:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -346,10 +414,79 @@ generator function running in a separate actor:
|
||||||
|
|
||||||
tractor.run(main)
|
tractor.run(main)
|
||||||
|
|
||||||
|
By default async generator functions are treated as inter-actor
|
||||||
|
*streams* when invoked via a portal (how else could you really interface
|
||||||
|
with them anyway) so no special syntax to denote the streaming *service*
|
||||||
|
is necessary.
|
||||||
|
|
||||||
|
|
||||||
|
Channels and Contexts
|
||||||
|
+++++++++++++++++++++
|
||||||
|
If you aren't fond of having to write an async generator to stream data
|
||||||
|
between actors (or need something more flexible) you can instead use a
|
||||||
|
``Context``. A context wraps an actor-local spawned task and a ``Channel``
|
||||||
|
so that tasks executing across multiple processes can stream data
|
||||||
|
to one another using a low level, request oriented API.
|
||||||
|
|
||||||
|
``Channel`` is the API which wraps an underlying *transport* and *interchange*
|
||||||
|
format to enable *inter-actor-communication*. In its present state ``tractor``
|
||||||
|
uses TCP and msgpack_.
|
||||||
|
|
||||||
|
As an example if you wanted to create a streaming server without writing
|
||||||
|
an async generator that *yields* values you instead define an async
|
||||||
|
function:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
async def streamer(ctx: tractor.Context, rate: int = 2) -> None:
|
||||||
|
"""A simple web response streaming server.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
val = await web_request('http://data.feed.com')
|
||||||
|
|
||||||
|
# this is the same as ``yield`` in the async gen case
|
||||||
|
await ctx.send_yield(val)
|
||||||
|
|
||||||
|
await trio.sleep(1 / rate)
|
||||||
|
|
||||||
|
|
||||||
|
All that's required is declaring a ``ctx`` argument name somewhere in
|
||||||
|
your function signature and ``tractor`` will treat the async function
|
||||||
|
like an async generator - as a streaming function from the client side.
|
||||||
|
This turns out to be handy particularly if you have
|
||||||
|
multiple tasks streaming responses concurrently:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
async def streamer(ctx: tractor.Context, rate: int = 2) -> None:
|
||||||
|
"""A simple web response streaming server.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
val = await web_request(url)
|
||||||
|
|
||||||
|
# this is the same as ``yield`` in the async gen case
|
||||||
|
await ctx.send_yield(val)
|
||||||
|
|
||||||
|
await trio.sleep(1 / rate)
|
||||||
|
|
||||||
|
|
||||||
|
async def stream_multiple_sources(
|
||||||
|
ctx: tractor.Context, sources: List[str]
|
||||||
|
) -> None:
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
for url in sources:
|
||||||
|
n.start_soon(streamer, ctx, url)
|
||||||
|
|
||||||
|
|
||||||
|
The context notion comes from the context_ in nanomsg_.
|
||||||
|
|
||||||
|
.. _context: https://nanomsg.github.io/nng/man/tip/nng_ctx.5
|
||||||
|
.. _msgpack: https://en.wikipedia.org/wiki/MessagePack
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A full fledged streaming service
|
A full fledged streaming service
|
||||||
********************************
|
++++++++++++++++++++++++++++++++
|
||||||
Alright, let's get fancy.
|
Alright, let's get fancy.
|
||||||
|
|
||||||
Say you wanted to spawn two actors which each pull data feeds from
|
Say you wanted to spawn two actors which each pull data feeds from
|
||||||
|
@ -471,58 +608,6 @@ as ``multiprocessing`` calls it) which is running ``main()``.
|
||||||
.. _remote function execution: https://codespeak.net/execnet/example/test_info.html#remote-exec-a-function-avoiding-inlined-source-part-i
|
.. _remote function execution: https://codespeak.net/execnet/example/test_info.html#remote-exec-a-function-avoiding-inlined-source-part-i
|
||||||
|
|
||||||
|
|
||||||
Cancellation
|
|
||||||
************
|
|
||||||
``tractor`` supports ``trio``'s cancellation_ system verbatim.
|
|
||||||
Cancelling a nursery block cancels all actors spawned by it.
|
|
||||||
Eventually ``tractor`` plans to support different `supervision strategies`_ like ``erlang``.
|
|
||||||
|
|
||||||
.. _supervision strategies: http://erlang.org/doc/man/supervisor.html#sup_flags
|
|
||||||
|
|
||||||
|
|
||||||
Remote error propagation
|
|
||||||
************************
|
|
||||||
Any task invoked in a remote actor should ship any error(s) back to the calling
|
|
||||||
actor where it is raised and expected to be dealt with. This way remote actors
|
|
||||||
are never cancelled unless explicitly asked or there's a bug in ``tractor`` itself.
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
async def assert_err():
|
|
||||||
assert 0
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async with tractor.open_nursery() as n:
|
|
||||||
real_actors = []
|
|
||||||
for i in range(3):
|
|
||||||
real_actors.append(await n.start_actor(
|
|
||||||
f'actor_{i}',
|
|
||||||
rpc_module_paths=[__name__],
|
|
||||||
))
|
|
||||||
|
|
||||||
# start one actor that will fail immediately
|
|
||||||
await n.run_in_actor('extra', assert_err)
|
|
||||||
|
|
||||||
# should error here with a ``RemoteActorError`` containing
|
|
||||||
# an ``AssertionError`` and all the other actors have been cancelled
|
|
||||||
|
|
||||||
try:
|
|
||||||
# also raises
|
|
||||||
tractor.run(main)
|
|
||||||
except tractor.RemoteActorError:
|
|
||||||
print("Look Maa that actor failed hard, hehhh!")
|
|
||||||
|
|
||||||
|
|
||||||
You'll notice the nursery cancellation conducts a *one-cancels-all*
|
|
||||||
supervisory strategy `exactly like trio`_. The plan is to add more
|
|
||||||
`erlang strategies`_ in the near future by allowing nurseries to accept
|
|
||||||
a ``Supervisor`` type.
|
|
||||||
|
|
||||||
.. _exactly like trio: https://trio.readthedocs.io/en/latest/reference-core.html#cancellation-semantics
|
|
||||||
.. _erlang strategies: http://learnyousomeerlang.com/supervisors
|
|
||||||
|
|
||||||
|
|
||||||
Actor local variables
|
Actor local variables
|
||||||
*********************
|
*********************
|
||||||
Although ``tractor`` uses a *shared-nothing* architecture between processes
|
Although ``tractor`` uses a *shared-nothing* architecture between processes
|
||||||
|
@ -556,8 +641,8 @@ a convenience for passing simple data to newly spawned actors); building
|
||||||
out a state sharing system per-actor is totally up to you.
|
out a state sharing system per-actor is totally up to you.
|
||||||
|
|
||||||
|
|
||||||
How do actors find each other (a poor man's *service discovery*)?
|
Service Discovery
|
||||||
*****************************************************************
|
*****************
|
||||||
Though it will be built out much more in the near future, ``tractor``
|
Though it will be built out much more in the near future, ``tractor``
|
||||||
currently keeps track of actors by ``(name: str, id: str)`` using a
|
currently keeps track of actors by ``(name: str, id: str)`` using a
|
||||||
special actor called the *arbiter*. Currently the *arbiter* must exist
|
special actor called the *arbiter*. Currently the *arbiter* must exist
|
||||||
|
@ -590,70 +675,6 @@ The ``name`` value you should pass to ``find_actor()`` is the one you passed as
|
||||||
*first* argument to either ``tractor.run()`` or ``ActorNursery.start_actor()``.
|
*first* argument to either ``tractor.run()`` or ``ActorNursery.start_actor()``.
|
||||||
|
|
||||||
|
|
||||||
Streaming using channels and contexts
|
|
||||||
*************************************
|
|
||||||
``Channel`` is the API which wraps an underlying *transport* and *interchange*
|
|
||||||
format to enable *inter-actor-communication*. In its present state ``tractor``
|
|
||||||
uses TCP and msgpack_.
|
|
||||||
|
|
||||||
If you aren't fond of having to write an async generator to stream data
|
|
||||||
between actors (or need something more flexible) you can instead use a
|
|
||||||
``Context``. A context wraps an actor-local spawned task and a ``Channel``
|
|
||||||
so that tasks executing across multiple processes can stream data
|
|
||||||
to one another using a low level, request oriented API.
|
|
||||||
|
|
||||||
As an example if you wanted to create a streaming server without writing
|
|
||||||
an async generator that *yields* values you instead define an async
|
|
||||||
function:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
async def streamer(ctx: tractor.Context, rate: int = 2) -> None:
|
|
||||||
"""A simple web response streaming server.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
val = await web_request('http://data.feed.com')
|
|
||||||
|
|
||||||
# this is the same as ``yield`` in the async gen case
|
|
||||||
await ctx.send_yield(val)
|
|
||||||
|
|
||||||
await trio.sleep(1 / rate)
|
|
||||||
|
|
||||||
|
|
||||||
All that's required is declaring a ``ctx`` argument name somewhere in
|
|
||||||
your function signature and ``tractor`` will treat the async function
|
|
||||||
like an async generator - as a streaming function from the client side.
|
|
||||||
This turns out to be handy particularly if you have
|
|
||||||
multiple tasks streaming responses concurrently:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
async def streamer(ctx: tractor.Context, rate: int = 2) -> None:
|
|
||||||
"""A simple web response streaming server.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
val = await web_request(url)
|
|
||||||
|
|
||||||
# this is the same as ``yield`` in the async gen case
|
|
||||||
await ctx.send_yield(val)
|
|
||||||
|
|
||||||
await trio.sleep(1 / rate)
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_multiple_sources(
|
|
||||||
ctx: tractor.Context, sources: List[str]
|
|
||||||
) -> None:
|
|
||||||
async with trio.open_nursery() as n:
|
|
||||||
for url in sources:
|
|
||||||
n.start_soon(streamer, ctx, url)
|
|
||||||
|
|
||||||
|
|
||||||
The context notion comes from the context_ in nanomsg_.
|
|
||||||
|
|
||||||
.. _context: https://nanomsg.github.io/nng/man/tip/nng_ctx.5
|
|
||||||
.. _msgpack: https://en.wikipedia.org/wiki/MessagePack
|
|
||||||
|
|
||||||
|
|
||||||
Running actors standalone
|
Running actors standalone
|
||||||
*************************
|
*************************
|
||||||
You don't have to spawn any actors using ``open_nursery()`` if you just
|
You don't have to spawn any actors using ``open_nursery()`` if you just
|
||||||
|
|
Loading…
Reference in New Issue