[openstack-dev] [oslo] Asyncio and oslo.messaging

Nikola Đipanov ndipanov at redhat.com
Mon Jul 7 10:48:59 UTC 2014


On 07/03/2014 05:27 PM, Mark McLoughlin wrote:
> Hey
> 
> This is an attempt to summarize a really useful discussion that Victor,
> Flavio and I have been having today. At the bottom are some background
> links - basically what I have open in my browser right now thinking
> through all of this.
> 
> We're attempting to take baby-steps towards moving completely from
> eventlet to asyncio/trollius. The thinking is for Ceilometer to be the
> first victim.
>
> </snip>
>
> When we switch to asyncio's event loop, all of this code needs to be
> ported to asyncio's explicitly asynchronous approach. We might do:
> 
>   @asyncio.coroutine
>   def foo(self):
>       result = yield from some_async_op(...)
>       return do_stuff(result)
> 
> or:
> 
>   @asyncio.coroutine
>   def foo(self):
>       fut = Future()
>       some_async_op(callback=fut.set_result)
>       ...
>       result = yield from fut
>       return do_stuff(result)
> 
> Porting from eventlet's implicit async approach to asyncio's explicit
> async API will be seriously time consuming and we need to be able to do
> it piece-by-piece.
> 
> The question then becomes what do we need to do in order to port a
> single oslo.messaging RPC endpoint method in Ceilometer to asyncio's
> explicit async approach?
> 
> The plan is:
> 
>   - we stick with eventlet; everything gets monkey patched as normal
> 
>   - we register the greenio event loop with asyncio - this means that 
>     e.g. when you schedule an asyncio coroutine, greenio runs it in a 
>     greenlet using eventlet's event loop
> 
>   - oslo.messaging will need a new variant of eventlet executor which 
>     knows how to dispatch an asyncio coroutine. For example:
> 
>         while True:
>             incoming = self.listener.poll()
>             method = dispatcher.get_endpoint_method(incoming)
>             if asyncio.iscoroutinefunc(method):
>                 result = method()
>                 self._greenpool.spawn_n(incoming.reply, result)
>             else:
>                 self._greenpool.spawn_n(method)
> 
>     it's important that even with a coroutine endpoint method, we send 
>     the reply in a greenthread so that the dispatch greenthread doesn't
>     get blocked if the incoming.reply() call causes a greenlet context
>     switch
> 
>   - when all of ceilometer has been ported over to asyncio coroutines, 
>     we can stop monkey patching, stop using greenio and switch to the 
>     asyncio event loop
> 
>   - when we make this change, we'll want a completely native asyncio 
>     oslo.messaging executor. Unless the oslo.messaging drivers support 
>     asyncio themselves, that executor will probably need a separate
>     native thread to poll for messages and send replies.
> 
> If you're confused, that's normal. We had to take several breaks to get
> even this far because our brains kept getting fried.
> 

Thanks Mark for putting this all together in an approachable way. This
is really interesting work, and I wish I found out about all of this
sooner :).

When I read all of this stuff and got my head around it (took some time
:) ), a glaring drawback of such an approach, and as I mentioned on the
spec proposing it [1] is that we would not really doing asyncio, we
would just be pretending we are by using a subset of it's APIs, and
having all of the really important stuff for overall design of the code
(code that needs to do IO in the callbacks for example) and ultimately -
performance, completely unavailable to us when porting.

So in Mark's example above:

  @asyncio.coroutine
  def foo(self):
    result = yield from some_async_op(...)
    return do_stuff(result)

A developer would not need to do anything that asyncio requires like
make sure that some_async_op() registers a callback with the eventloop
(using for example event_loop.add_reader/writer methods) you could just
simply make it use a 'greened' call and things would continue working
happily. I have a feeling this will in turn have a lot of people writing
code that they don't understand, and as library writers - we are not
doing an excellent job at that point.

Now porting an OpenStack project to another IO library with completely
different design is a huge job and there is unlikely a single 'right'
way to do it, so treat this as a discussion starter, that will hopefully
give us a better understanding of the problem we are trying to tackle.

So I hacked up together a small POC of a different approach. In short -
we actually use a real asyncio selector eventloop in a separate thread,
and dispatch stuff to it when we figure out that our callback is in fact
a coroutine. More will be clear form the code so:

(Warning - hacky code ahead): [2]

I will probably be updating it - but if you just clone the repo, all the
history is there. I wrote it without the oslo.messaging abstractions
like listener and dispatcher, but it is relatively easy to see which
bits of code would go in those.

Several things worth noting as you read the above. First one is that we
do not monkeypatch until we have fired of the asyncio thread (Victor
correctly noticed this would be a problem in a comment on [1]). This may
seem hacky (and it is) but if decide to go further down this road - we
would probably not be 'greening the world' but rather importing patched
non-ported modules when we need to dispatch to them. This may sound like
a big deal, and it is, but it is critical to actually running ported
code in a real asyncio evenloop. I have not yet tested this further, but
from briefly reading eventlet code - it seems like ti should work.

Another interesting problem is (as I have briefly mentioned in [1]) -
what happens when we need to synchronize between eventlet-run and
asyncio-run callbacks while we are in the process of porting. I don't
have a good answer to that yet, but it is worth noting that the proposed
approach doesn't either, and this is a thing we should have some idea
about before going in with a knife.

Now for some marketing :) - I can see several advantages of such an
approach, the obvious one being as stated, that we are in fact doing
asyncio, so we are all in. Also as you can see [2] the implementation is
far from magical - it's (surprisingly?) simple, and requires no other
additional dependencies apart from trollius itself (granted greenio is
not too complex either). I am sure that we would hit some other problems
that were not clear from this basic POC (it was done in ~3 hours on a
bus), but it seems to me that those problems will likely need to be
solved anyhow if we are to port Ceilometer (or any other project) to
asyncio, we will just hit them sooner this way.

It was a fun approach to ponder anyway - so I am looking forward to
comments and thoughts.

Thanks,

Nikola

[1] https://review.openstack.org/#/c/104792/
[2]
https://github.com/djipko/eventlet-asyncio/blob/master/asyncio_dispatch.py

> HTH,
> Mark.
> 
> Victor's excellent docs on asyncio and trollius:
> 
>   https://docs.python.org/3/library/asyncio.html
>   http://trollius.readthedocs.org/
> 
> Victor's proposed asyncio executor:
> 
>   https://review.openstack.org/70948
> 
> The case for adopting asyncio in OpenStack:
> 
>   https://wiki.openstack.org/wiki/Oslo/blueprints/asyncio
> 
> A previous email I wrote about an asyncio executor:
> 
>  http://lists.openstack.org/pipermail/openstack-dev/2013-June/009934.html
> 
> The mock-up of an asyncio executor I wrote:
> 
>   https://github.com/markmc/oslo-incubator/blob/8509b8b/openstack/common/messaging/_executors/impl_tulip.py
> 
> My blog post on async I/O and Python:
> 
>   http://blogs.gnome.org/markmc/2013/06/04/async-io-and-python/
> 
> greenio - greelets support for asyncio:
> 
>   https://github.com/1st1/greenio/
> 
> 
> _______________________________________________
> OpenStack-dev mailing list
> OpenStack-dev at lists.openstack.org
> http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
> 




More information about the OpenStack-dev mailing list