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
|
||||
|
||||
|
||||
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
|
||||
borrowed_ from ``trio``. A portal may seem similar to the idea of
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -346,10 +414,79 @@ generator function running in a separate actor:
|
|||
|
||||
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
|
||||
********************************
|
||||
++++++++++++++++++++++++++++++++
|
||||
Alright, let's get fancy.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
*********************
|
||||
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.
|
||||
|
||||
|
||||
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``
|
||||
currently keeps track of actors by ``(name: str, id: str)`` using a
|
||||
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()``.
|
||||
|
||||
|
||||
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
|
||||
*************************
|
||||
You don't have to spawn any actors using ``open_nursery()`` if you just
|
||||
|
|
Loading…
Reference in New Issue