Con las antiguas limitaciones del GAE, la ejecución de un proceso batch (generación de informes, actualización de una columna en todas las entidades de un tipo...) requería montar complejos sistemas diviendo nuestro proceso en pequeñas partes que serían procesadas por tareas enlazadas.

Desde hace poco (SDK 1.4.0.) el límite de tiempo para las peticiones en segundo plano (tareas y cron) aumentó:  ¡de 30 segundos a 10 minutos! , por lo que,  la mayoría de los procesos antes mencionados podrían ejecutarse sin mayor problema.

Sin embargo, cuando lo que necesitamos es recorrer todas las entidades de un modelo y actualizarlas/operar con ellas, mapreduce nos aporta algunas ventajas:

  • Comodidad a la hora de definir un nuevo script (mapreduce handler).
  • Eficiencia: mapreduce se ejecuta atutomáticamente con un número configurable de shards.
  • Escalabilidad infinita (aquí no tenemos la limitación de 10 minutos en caso de tener millones de entidades).

El problema es que los mapreduce están pensados para lanzarse manualmente desde la consola de administración de mapreduce (url_de_mi_aplicacion/mapreduce) pero en ocasiones nos gustaría poder lanzarlos programáticamente. Estudiando un poco el funcionamiento de esta consola y el código de mapreduce es sencillo modificarlo para conseguir nuestro propósito. Por ejemplo, lanzar un mapreduce desde un cron que se ejecute todos los lunes a la misma hora.

Lo primero es definir la entrada en nuestro cron.yaml, por ejemplo:

cron:
- description: purge tokens
 url: /admin/cron/execute_mapreduce
 schedule: every monday 00:00
 timezone: Europe/Madrid

Nosotros usamos Django, así que definiríamos una entrada en urls.py para mapear la url definida en el cron con nuestro método python encargado de llamar al mapreduce:

(r'^admin/cron/execute_mapreduce$', 'ruta_del_modulo.run_map_reduce'),

Y ahora viene la duda. ¿Cómo llamamos al mapreduce desde dicho método si sólo se puede a través de la consola? Esstudiando el comportamiento de la consola con una herramienta tipo firebug, vemos que cuando se pulsa en el botón "run" se ejecuta una llamada AJAX con una serie de parámetros. Si replicamos dicha llamada en nuestro código ya tenemos el mismo comportamiento.

Si  la definición de nuestro mapreduce (en el mapreduce.yaml) es ésta:

- name: execute_something_from_cron
 mapper:
 input_reader: mapreduce.input_readers.DatastoreInputReader
 handler: map_reduce_handlers.executed_from_cron.execute_something_from_cron
 params:
 - name: entity_kind
 default: points_of_sale.models.pos_model.PointOfSale

Entonces la llamada por código (en localhost) sería esta:

 form_fields = {
 "mapper_handler": "map_reduce_handlers.executed_from_cron.execute_something_from_cron",
 "mapper_input_reader": "mapreduce.input_readers.DatastoreInputReader",
 "mapper_params.entity_kind": "points_of_sale.models.pos_model.PointOfSale",
 "name": "calculate_aggregates_of_aggregates"
 }

 form_data = urllib.urlencode(form_fields)
 result = urlfetch.fetch(url="http://localhost:8080/mapreduce/command/start_job",
 payload=form_data,
 method=urlfetch.POST,
 headers={'Content-Type': 'application/x-www-form-urlencoded'},
 deadline=10)

Por último, para probar que todo funciona ya sólo queda ejecutar el cron. En local se puede hacer a través de la url:

http://localhost:8080/_ah/admin/cron

Pero cuidado, para poder ejecutarlo en local necesitamos un pequeño cambio. Lo normal es que mapreduce requiera permisos de administrador, lo cual, se indica en app.yaml:

- url: /mapreduce(/.*)?
 script: mapreduce/main.py
 login: admin  

Si ejecutamos el cron en la nube no hay problema porque tiene permisos de admin, sin embargo, el test local se ejecuta con permisos de usuario y, por ello, es necesario comentar la línea "login: admin" para las pruebas locales.

Ya parece que tenemos todos lo necesario, así que, lanzamos el test del cron desde el nagegador...obteniendo el siguiente resultado:

ApplicationError: 2 timed out

¿Por qué ocurre esto? La explicación se encuentra en el fichero base_handler.py de la libería mapreduce. Concretamente en el método _handle_wrapper:

def _handle_wrapper(self):
 if self.request.headers.get("X-Requested-With") != "XMLHttpRequest":
 logging.error(self.request.headers)
 logging.error("Got JSON request with no X-Requested-With header")
 self.response.set_status(
 403, message="Got JSON request with no X-Requested-With header")
 return

Como vemos, se están vetando explícitamente aquellas peticiones que no sean AJAX.

Afortunadamente, existe una solución muy fácil que evita cambiar el código de la librería mapreduce (sería peligroso porque en cualquier momento puede cambiar de versión o incorporarse por defecto sin necesidad de incluirla en el despliegue): se trata de añadir la cabecera XMLHttpRequest a nuestra petición:

headers={'Content-Type': 'application/x-www-form-urlencoded',
 'X-Requested-With': "XMLHttpRequest"},

Y ahora ya podemos ejecutar el mapreduce desde el cron y consultar la consola/logs cuando queramos ver su resultado. También se podrían lanzar peticiones (al igual que lo hace la consola) para comprobar que todo ha ido bien por código, pero eso ya es otra historia...