[Web-SIG] [PEP 444] Future- and Generator-Based Async Idea
P.J. Eby
pje at telecommunity.com
Sat Jan 8 18:00:18 CET 2011
At 03:26 AM 1/8/2011 -0800, Alice BevanMcGregor wrote:
>Warning: this assumes we're running on bizzaro-world PEP 444 that
>mandates applications are generators. Please do not dismiss this
>idea out of hand but give it a good look and maybe some feedback. ;)
First-glance feedback: I'm impressed. You may have something going
here after all. I just wish you'd sent this sooner. ;-)
I can easily see why I didn't think of this myself: I hadn't shifted
my thinking to accomodate for two important changes in the Python
environment since the first WSGI spec, circa 2003-04:
1. Coroutines and decorators are ubiquitous and non-intrusive
2. WSGI has stdlib support, and in any event it is much easier to
rely on non-stdlib packages
My major concern about the approach is still that it requires a fair
amount of overhead on the part of both app developers and middleware
developers, even if that overhead mostly consists of importing and
decorating. (More below.)
>The second middleware demonstration (using a decorator) makes
>middleware look a lot more like an application: yielding futures, or
>a response, with the addition of yielding an application callable
>not explored in the first (long, but trivial) example. I believe
>this should cover 99% of middleware use cases, including interactive
>debugging, request routing, etc. and the syntax isn't too bad, if
>you don't mind standardized decorators.
If we assume that the implementation would be in a wsgi2ref for
Python 3.3 and distributed standalone for 2.x, I think we can make
something work. (In the sense of practical to implement, not
necessarily *desirable*.)
One of my goals is that it should be possible to write "async-naive"
applications and middleware, so that people who don't care about
async can ignore it.
On the application side, this is easy: a trivial decorator suffices
to translate a return into a yield.
For middleware, it's not quite as simple, unless you have a pure
ingress or egress filter, since you can't simply "call" the
application. However, a "context manager"-like pattern applies,
wherein you could simply yield to calling a wrapped version of the application.
Hm. This seems to pretty much generalize to a standard
coroutine/trampoline pattern, where the server provides the
trampoline, and can provide APIs in the environ to create waitable
objects that can be yielded upward.
Actually, this is kind of like what I really wanted the futures PEP
to be about. And it also preserves composability nicely.
In fact, it doesn't actually need any middleware decorators, if the
server provides the trampoline.
We would leave your "my_awesome_application" example intact (possibly
apart from having a friendlier API for reading from wsgi.input), but
change my_middleware as follows:
def my_middleware(app):
def wrapper(environ):
# pre-response code here
response = yield app(environ)
# post-response code here
yield altered_response
return wrapper
That's it. No decorators, no nothing.
The server-level trampoline is then just a function that looks
something like this:
def app_trampoline(coroutine, yielded):
if [yielded is a future of some sort]:
[arrange to invoke 'coroutine(result)' upon completion]
[arrange to inovke 'coroutine(None, exc_info)' upon error]
return "pause"
elif [yielded is a response]:
return "return"
elif [yielded has send/throw methods]:
return "call" # tell the coroutine to call it
else:
raise TypeError
The trampoline function is used with a coroutine class like this:
class Coroutine:
def __init__(self, iterator, trampoline, callback):
self.stack = [iterator]
self.trampoline = trampoline
self()
def __call__(self, value=None, exc_info=()):
stack = self.stack
while stack:
try:
it = stack[-1]
if exc_info:
try:
rv = it.throw(*exc_info)
finally:
exc_info = ()
else:
rv = it.send(value)
except BaseException:
value = None
exc_info = sys.exc_info()
if exc_info[0] is StopIteration:
# pass return value up the stack
value, = exc_info[1].args or (None,)
exc_info = () # but not the error
stack.pop()
else:
switch = self.trampoline(self, rv)
if switch=="pause":
return
elif switch=="call":
stack.append(rv) # Call subgenerator
value, exc_info = None, ()
elif switch=="return":
value, exc_info = rv, ()
stack.pop()
# Coroutine is entirely finished
self.callback(value)
And run by simply calling:
Coroutine(app(environ), app_trampoline, process_response)
Where process_response() is a function receiving a three-tuple to
process the actual result.
That's basically it. The Coroutine class is
server/framework-independent; the minimal trampoline function is the
part the server author has to write.
The body iterator can follow a similar protocol, but the trampoline
function is different:
def body_trampoline(coroutine, yielded):
if type(yielded) is bytes:
if len(coroutine.stack)==1: # only accept from
outermost middleware
[send the bytes out]
[arrange to invoke coroutine() when send is completed]
return "pause"
else:
return "return"
if [yielded is a future of some sort]:
[arrange to invoke 'coroutine(result)' upon completion]
[arrange to inovke 'coroutine(None, exc_info)' upon error]
return "pause"
elif [yielded has send/throw methods]:
return "call" # tell the coroutine to call it
else:
raise TypeError
So, part of the server's "process_response" callback would look like:
Coroutine(body_iter, body_trampoline, finish_response)
You can then implement response-processing middleware like this:
def latinize_body(body_iter):
while True:
chunk = yield body_iter
if chunk is None:
break
else:
yield piglatin(yield body_iter)
def piglatin(app):
def wrapper(environ):
s, h, b = yield app(environ)
if [suitable for processing]:
yield s, h, latinize_body(b)
else:
yield s, h, b # skip body processing
My overall impression is still that there's something worth
considering here, but there is still some ugly mental overheads
involved for body-processing middleware, if we want to support
pausing during the body iteration. The latinize_body function above
isn't exactly intuitively obvious, compared to a for loop, and it
can't be replaced by one without using greenlets.
On the plus side, it can actually all be done without any decorators at all.
(The next interesting challenge would be to integrate this with
Graham's proposal for adding cleanup handlers...)
More information about the Web-SIG
mailing list