How do i use argon2 asynchronously with python tornado?

476 Views Asked by At

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.

1

There are 1 best solutions below

5
On

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.