Going asynchronous: from Flask to Twisted Klein



Introduction to Twisted Klein, which is like Flask, but allows running asynchronous code.

Preface

This post (part 1/2) is an introduction/tutorial for Twisted Klein and assumes basic knowledge of Python,Flask and Twisted. If you don't know Twisted, here is a good introduction.

Move on to part 2:

Related:

  • Twisted Mixing: A talk by Laurens Van Houtven at PyCon 2014
  • Crochet: A library for running Twisted code from blocking/threaded applications

Introduction

Flask is a Python microframework for the Web. I love it. It's incredible fun to to use and gets out of your way. It's light-weight, and works great when combined with Jinja2 for templating.

However, there is one thing that is limiting:

I cannot use non-blocking, asynchronous code in my request handlers


Why is that?

Flask, like DjangoPyramid and others, are based on WSGI, an API standard for connecting Python Web frameworks to Web servers. Which is great. However, WSGI is a (fundamentally) synchronous and blocking API.

WSGI was designed for the old fashioned, threaded/pre-forked Web servers (think Apache). It does not blend well with the new breed of asynchronous networking frameworks like Twistedasyncio orTornado.

Why is that limiting?

A (single thread/process) of a WSGI based Web server can only handle one request at a time. The code serving a request will run to its end before another request can be served.

Which means if you want to do e.g. something network related within your request handler, like calling out to a Web service, this will again block the incoming request, and in fact the complete thread/process until the outgoing request comes back.

With more and more requests being blocked, this can quickly lead to a situation where all worker threads/processes are blocked, and no incoming request is served at all anymore. Bad, bad.

Please note that this not only applies to doing Web service requests in your request handlers, but any kind of I/O operation which might take longer, like file or database access.

Can we fix it?

Yes, we can ;) We first show an example using Flask with synchronous code. Then we show how to do the same with asynchronous code using Twisted Klein.

The Synchronous World

We'll first go through a trivial Flask app to demonstrate the issue. In the next section we'll see how to do the same using Twisted Klein, fixing the issue.

To run the following examples, you will need to install:

pip install flask requests

Consider the following Flask app:

from flask  import Flask, request
 
app  = Flask(__name__)
 
 
@app .route( '/square/submit' , methods  = [ 'POST' ])
def square_submit():
    = int (request.form.get( 'x' 0 ))
    res  = * x
    return "{} squared is {}" . format (x, res)
 
 
if __name__  = = "__main__" :
    app.run(port  = 8080 , debug  = True )

This app will respond to HTTP/POST requests sent to /square/submit, extract the form variable x from the request, square that number and return a text response.

E.g. you can test it with

<!DOCTYPE html>
< html >
    < body >
       < form action = "http://localhost:8080/square/submit" method = "post" >
          < p >
             Square this < input type = "number" name = "x" value = "23" />
          </ p >
          < p >
             < input type = "submit" />
          </ p >
       </ form >
    </ body >
</ html >

Now, what if you want to use a Web service to actually compute the square?

Please note, this is an example - normally you wouldn't call out to a Web service to compute a square ;) In fact, we do a simple HTTP/GET and not a full flavored Ajax whatever Web service request. It's just an example.

Consider this:

import requests
from flask  import Flask, request
 
app  = Flask(__name__)
 
 
@app .route( '/square/submit' , methods  = [ 'POST' ])
def square_submit():
    = int (request.form.get( 'x' 0 ))
 
    ## simulate calling out to some Web service:
    ##
    = requests.get( 'http://www.tavendo.com/' )
    = len (r.text)
 
    res  = * + y
    return "{} squared plus {} is {}" . format (x, y, res)
 
 
if __name__  = = "__main__" :
    app.run(port  = 8080 , debug  = True )

The requests.get method simulates calling out to some Web service. This call will block until the outgoing HTTP request comes back. And hence, the client's original request to /square/submit will also block. And that means: the whole thread/process will block and cannot do more work like serving other incoming requests while still waiting on outgoing requests.

This can quickly get problematic: when calling out to the Web service takes some time, more and more requests will block until all threads or processes of the WSGI container running the app are blocked.

At that point, no new client requests will be served at all!

Note: It's quite common for WSGI containers to run something like 10 worker threads or processes. That means the app above will stall as soon as 10 incoming requests are served that each still wait on their outgoing request to return. And 10 isn't a large number ;)

Going Asynchronous

Alright, now that we've seen the issue, let's migrate to Twisted Klein and go asynchronous.

To run the following examples, you will need to install:

pip install twisted klein treq

Here is how the first Flask example from above looks when converted to Twisted Klein:

from klein  import Klein
 
app  = Klein()
 
 
@app .route( '/square/submit' , methods  = [ 'POST' ])
def square_submit(request):
    = int (request.args.get( 'x' , [ 0 ])[ 0 ])
    res  = * x
    return "{} squared is {}" . format (x, res)
 
 
if __name__  = = "__main__" :
    app.run( "localhost" 8080 )

Alright. Nothing spectacular. It looks almost identical to the Flask version. The most visible difference is that our request handler square_submit() now takes an request arguments provided by Klein, whereas Flask has request as a global object.

Now, the more interesting second Flask example looks like this when converted to Klein (don't worry, I'll go through the details below):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import treq
from klein  import Klein
from twisted.internet.defer  import inlineCallbacks, returnValue
 
app  = Klein()
 
 
@app .route( '/square/submit' , methods  = [ 'POST' ])
@inlineCallbacks
def square_submit(request):
    = int (request.args.get( 'x' , [ 0 ])[ 0 ])
 
    ## simulate calling out to some Web service:
    ##
    = yield treq.get( 'http://www.tavendo.com/' )
    content  = yield r.content()
    = len (content)
 
    res  = * + y
    returnValue( "{} squared plus {} is {}" . format (x, y, res))
 
 
if __name__  = = "__main__" :
    app.run( "localhost" 8080 )

We are doing an (asynchronous) outgoing HTTP request on line 15. For this, we are using treq, a Twisted port of Requests, a HTTP client library that is easy to use.

The request treq.get() will not return immediately (since we are doing a network request), but it won't block (since we are in asynch land).

It will return a Twisted Deferred (elsewhere known as Futures). The Deferred stands for a result of an asynchronous operation, where the actual return value of the operation will only be available later.

To simplify the look and feel of the code, we are using yieldinlineCallbacks and returnValuefollowing a specific variant of asynchronous programming ("co-routines"). This makes the code "look" synchronous, but nevertheless execute asynchronous under the hood.

Once the HTTP request returns (actually, the HTTP response header), the result of treq.get() becomes available in our variable r, which will hold a response object.

To actually retrieve the response body of our request, we now yield from r.content() (line 16).

There are only two other noteworthy details here: we need to decorate the function withinlineCallbacks to make the co-routine style available (line 9), and we cannot return directly, but use the returnValue wrapper (line 20).

Note: The reason returnValue is necessary goes deep into implementation details of Twisted and Python. In short: co-routines in Python 2 with Twisted are simulated using exceptions. Only Python 3.3+ has gotten native support for co-routines using the new yield from statement.

What have we gained?

Above variant using Twisted Klein will sustain a very high number of concurrent requests. It won't block. It will do so even on one thread, running on one CPU core! We finally are able to use asynchronous code inside Web request handlers. Awesome!

What's the downside?

The downside (obviously) is that you need to write your Web code on top of Twisted Klein, not Flask. At surface, Twisted Klein is very similar to Flask - however it is not a fully compatible drop-in replacement.

Summary

Flask is a great Web framework, and Twisted Klein makes the fun available in fully asynchronous Web applications. Going asynchronous allows you to do powerful things inside your request handlers - without running into performance problems due to blocking.

More

Read more in part 2: Mixing Web and WAMP code with Twisted Klein.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值