Handlers and Threads
One Waiter, Many Tables
Picture a busy café with just one waiter. The waiter takes an order at table 1, hands it to the kitchen, and -- instead of standing there waiting -- goes to table 2, then table 3. When the kitchen rings the bell, the waiter carries the food out. One waiter can serve a whole room because they never stand still waiting.
PyWeb's new App runs on PyNet's event loop, and the loop is that
waiter. Every request is a table. A handler is the recipe for one
dish (one page).
There are three kinds of handler, and they treat the waiter differently:
| Kind | How you write it | What the waiter does |
|---|---|---|
| async | async def |
Starts it, and while it awaits something, serves other tables. |
| plain | def |
Cooks it personally, right now, start to finish. |
| blocking | def with blocking=True |
Hands it to a helper in the back kitchen (a thread) and keeps serving. |
Warning: a plain def handler blocks everyone
While a plain
defhandler runs, nobody else is served. At all.The waiter is busy cooking your dish, so every other table waits -- every other request, on every connection. A plain
defhandler that takes one second makes the whole server freeze for one second.
Plain def handlers are fine for quick jobs (building a small page,
adding up a few numbers). If a handler does something slow that you
cannot await -- time.sleep, reading a huge file, a slow database
library -- mark it blocking=True, so it runs on a helper thread
using pynet.loop.run_in_thread.
There are only four helpers in the back kitchen (PyNet's thread
pool), and they are shared: the same four also read files for
mount_static. A fifth slow job has to queue until a helper is free. So
if many visitors hit slow blocking=True handlers at once, everyone
who needs a helper waits -- even someone just asking for a picture.
Keep blocking jobs as short as you can, and prefer async def with
await where a library lets you.
blocking=True only works on a plain def. Putting it on an
async def is an error (ValueError), because an async handler should
await instead of blocking.
Try It
import time
from pynet.loop import run, spawn
from pyweb.app import App
from pyweb.testing import TestClient
app = App()
@app.get("/fast")
async def fast(request):
return {"speed": "fast"}
@app.get("/hello/<name>")
def hello(request, name):
return f"<h1>Hello, {name}!</h1>"
@app.get("/report", blocking=True)
def report(request):
time.sleep(0.2) # pretend this is a slow database query
return {"report": "ready"}
async def main():
client = TestClient(app)
print((await client.get("/hello/Zoe")).text)
slow = spawn(client.get("/report"))
print((await client.get("/fast")).json())
print((await slow).json())
run(main())
Output:
The /report handler sleeps on a helper thread, so /fast is answered
while it is still working.
Path Parameters Become Arguments
A pattern like /hello/<name> or /users/<int:id> hands the captured
values to your handler as keyword arguments, already converted:
<int:id> gives you a real int. They are also kept in
request.path_params.
What Handlers May Return
| You return | You get |
|---|---|
an HttpResponse (from text, html, json, redirect, ...) |
sent as it is |
| a streaming response (such as server-sent events) | sent as it is |
a dict or list |
a JSON response |
a str |
an HTML page |
| anything else | a 500 Internal Server Error (and a log message) |
NaN and infinity are not real JSON, so a dict holding
float("nan") is a logged 500 too, and a request body containing
NaN or Infinity makes request.json() answer 400. So does a body that nests lists or
objects more than MAX_JSON_NESTING (100) levels deep, which no honest
client needs; the check is the same on every platform.
To answer with an error on purpose, raise HTTPError(409, "already
taken"). If your handler crashes with any other exception, the
visitor gets a plain 500 with no traceback (secrets stay secret),
and the full traceback is logged on the pyweb.app logger for you.
A HEAD request is answered by the GET handler; the App keeps the
headers and throws the body away.
What We Test
async defhandlers are awaited; plaindefhandlers run on the loop's own thread.- A
blocking=Truehandler runs on a different thread, and another request is answered while it is still running. blocking=Trueon anasync defis refused.<int:...>parameters arrive asintkeyword arguments.dict/listbecome JSON,strbecomes HTML, anything else is a logged500; so is adictholdingNaN, andNaNorInfinityin a request body is a400.HTTPError(409)answers409; a crash answers a generic500with no traceback in the body, and is logged.- A wrong method answers
405with anAllowheader such asGET, HEAD;HEADgetsGET's headers with an empty body. - Startup hooks run in order, shutdown hooks in reverse.
Next Up
Want to do something for every request? See Middleware.
See also
- PyNet: the event loop -- the loop PyWeb runs on, built from scratch.