0

Here is the handler for my login page, which i intend to use via ajax post requests.

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

class AdminLoginHandler(RequestHandler):
    async def post(self):
        username = self.get_argument("username")
        password = self.get_argument("password")
        db_hash =  await self.settings['db'].users.find_one({"username":username}, {"password":1})
        if not db_hash:
            await self.settings['hasher'].verify("","")
            self.write("wrong")
            return
        try:
            print(db_hash)
            pass_correct = await self.settings['hasher'].verify(db_hash['password'], password)
        except VerifyMismatchError:
            pass_correct = False
        if pass_correct:
            self.set_secure_cookie("user", username)
            self.write("set?")
        else:
            self.write("wrong")

The settings includes this argument hasher=PasswordHasher().

I'm getting the following error TypeError: object bool can't be used in 'await' expression, i'm aware this is because the function i'm calling doesn't return a future object but a boolean.

My question is how do i use the hashing library asynchronously without tornado blocking for the full time of the hashing process, which i know by design takes a long time.

  • If your question was answered please mark it as [solved](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work). – Ionut Ticus Apr 21 '20 at 15:37

1 Answers1

2

You can use a ThreadPoolExecutor or a ProcessPoolExecutor to run the time consuming code in separate threads/processes:

import math
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

import tornado.ioloop
import tornado.web


def blocking_task(number):
    return len(str(math.factorial(number)))


class MainHandler(tornado.web.RequestHandler):

    executor = ProcessPoolExecutor(max_workers=4)
    # executor = ThreadPoolExecutor(max_workers=4)

    async def get(self):
        number = 54545  # factorial calculation takes about one second on my machine
        # result = blocking_task(number)  # use this line for classic (non-pool) function call
        result = await tornado.ioloop.IOLoop.current().run_in_executor(self.executor, blocking_task, number)
        self.write("result has %d digits" % result)


def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

I used a simple factorial calculation here to simulate a CPU intensive task and tested the above using wrk:

wrk -t2 -c4 -d30s http://127.0.0.1:8888/
Running 30s test @ http://127.0.0.1:8888/
  2 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.25s    34.16ms   1.37s    72.04%
    Req/Sec     2.54      3.40    10.00     83.75%
  93 requests in 30.04s, 19.89KB read
Requests/sec:      3.10
Transfer/sec:     678.00B

Without the executor I would get around 1 requests/sec; of course you need to tune the max_workers setting according to your setup.
If you're going to test using a browser, be aware of possible limitations.

Edit

I modified the code to easily allow for a process executor instead of a thread executor, but I doubt it will make a lot of difference in your case mainly because calls to argon2 should release the GIL but you should test it nonetheless.

Community
  • 1
  • 1
Ionut Ticus
  • 2,683
  • 2
  • 17
  • 25
  • thank you for your effort. i know that tornado is typically run with one process per core, if i create new threads am i not undoing the effort of tornado. I'm thinking i might have to write a tool that works in a similar way to motor, using tornado's ioloop i think. – Felix Farquharson Apr 14 '20 at 14:26
  • i see you are using the ioloop i will award the bounty anyway – Felix Farquharson Apr 14 '20 at 14:30
  • 1
    The `ioloop.run_in_executor` call just schedules the function to run in the pool executor (the function itself does not run on the ioloop and does not block it). – Ionut Ticus Apr 14 '20 at 14:37
  • 1
    Using separate threads does not affect tornado when used properly; actually using a `thread/process` pool is the recommended way to run blocking code when async libs are not available; see *point 3* on Tornado's [FAQ](https://www.tornadoweb.org/en/stable/faq.html) – Ionut Ticus Apr 14 '20 at 14:44
  • happy to hear that; I benchmarked `argon2` as well but it's a bit misleading because it will use *multiple threads* by itself which means **requests per second** is similar between *executor* call and *classic blocking* call BUT for the blocking call the *ioloop* will be blocked most of the time (even a trivial request takes 2-3 seconds) while with the executor call it's always responsive (the trivial request takes 3-4 milliseconds). – Ionut Ticus Apr 19 '20 at 10:56