Cachear y reutilizar un resultado de función en Tornado

Tengo una función costosa para incluir en mi aplicación Tornado. La función devuelve varias salidas pero, por razones heredadas, estas salidas se acceden por separado a través de diferentes controladores.

¿Hay una manera de ejecutar la función solo una vez, reutilizar el resultado para los diferentes manejadores y preservar el comportamiento asíncrono de Tornado?

from tornado.web import RequestHandler from tonado.ioloop import IOLoop # the expensive function def add(x, y): z = x + y return x, y, z # the handlers that reuse the function class Get_X(RequestHandler): def get(self, x, y): x, y, z = add(x, y) return x class Get_Y(RequestHandler): def get(self, x, y): x, y, z = add(x, y) return y class Get_Z(RequestHandler): def get(self, x, y): x, y, z = add(x, y) return z # the web service application = tornado.web.Application([ (r'/Get_X', Get_X), (r'/Get_Y', Get_Y), (r'/Get_Z', Get_Z), ]) application.listen(8888) IOLoop.current().start() 

Pensé en almacenar en caché el resultado de la función en un diccionario, pero no estoy seguro de cómo hacer que los otros dos controladores esperen, mientras que el primero crea una entrada de diccionario.

Los Futures tornado son reutilizables, por lo que simplemente puedes guardar el Future antes de rendirlo. Muchos decoradores de almacenamiento en caché functools.lru_cache para functools.lru_cache (como functools.lru_cache python 3.2 solo funcionarán si los @gen.coroutine delante de @gen.coroutine :

 import functools from tornado import gen from tornado.ioloop import IOLoop @functools.lru_cache(maxsize=100) @gen.coroutine def expensive_function(): print('starting expensive_function') yield gen.sleep(5) return 1, 2, 3 @gen.coroutine def get_x(): print('starting get_x') x, y, z = yield expensive_function() return x @gen.coroutine def get_y(): print('starting get_y') x, y, z = yield expensive_function() return y @gen.coroutine def get_z(): print('starting get_z') x, y, z = yield expensive_function() return z @gen.coroutine def main(): x, y, z = yield [get_x(), get_y(), get_z()] print(x, y, z) if __name__ == '__main__': IOLoop.current().run_sync(main) 

Huellas dactilares:

 starting get_x starting expensive_function starting get_y starting get_z finished expensive_function 1 2 3 

Le preocupa que un manejador se tome el tiempo para calcular el valor que se colocará en el caché, mientras que otros manejadores esperan que el valor aparezca en el caché.

Tornado 4.2 incluye una clase de evento que puede usar para coordinar las coroutinas que desean el valor en caché. Cuando un manejador desea obtener un valor de la memoria caché, comprueba si el valor almacenado en caché está allí:

 from tornado import locks class Get_X(RequestHandler): @gen.coroutine def get(self, x, y): key = (x, y, 'Get_X') if key in cache: value = cache[key] if isinstance(value, locks.Event): # Another coroutine has begun calculating. yield value.wait() value = cache[key] self.write(value) return # Calculate the value. cache[key] = event = locks.Event() value = calculate(x, y) cache[key] = value event.set() self.write(value) 

Este código no está probado.

En código real, debe ajustar el calculate en un bash / excepto que borra el Evento de la caché si el calculate falla. De lo contrario, todos los demás coroutines esperarán eternamente a que se establezca el Evento.

Supongo que calculate devuelve una cadena que puede pasar a self.write . En su aplicación, puede haber un procesamiento adicional con el valor antes de que pueda llamar self.write o self.render .

También debe considerar qué tan grande puede crecer su caché: ¿qué tan grandes son los valores y cuántas claves distintas habrá? Es posible que necesite un caché acotado que desaloje el valor utilizado menos recientemente; Hay un montón de resultados de búsqueda para “Python LRU cache”, puedes probar con el de Raymond Hettinger ya que es muy respetado.

Para ver un ejemplo más sofisticado de RequestHandlers que usan eventos para sincronizar alrededor de un caché, vea mi ejemplo de proxy en la documentación de Toro . Está lejos de ser un proxy web con todas las funciones, pero el ejemplo está escrito para demostrar una solución al problema exacto que presenta: cómo evitar el trabajo duplicado al calcular un valor para colocarlo en el caché.