June 18, 2022

Rake y precondiciones arbitrarias

Rake es una herramienta muy poderosa que nos permite automatizar tareas repetitivas de manera eficiente.

En el artículo Rake, dependencias y archivos doy una pequeña introducción de Rake. Si no estás familiarizado con Rake, te invito a leer ese artículo antes de continuar con este.

La eficiencia de Rake es evidente sobre todo cuando lo utilizamos para derivar archivos a partir de archivos fuente.

Pero esa eficiencia aparentemente limitada a tareas relacionadas con archivos, puede ser explotada para cualquier tipo de tarea arbitraria.

En este artículo pretendo explicar cómo lograrlo.

Para ello empecemos repasando cómo Rake nos brinda dicha eficiencia cuando trabajamos con archivos.

Cómo Rake omite la reconstrucción innecesaria de archivos

Una característica bien conocida de Rake es que puede ahorrar tiempo evitando ejecutar tareas que dependen de archivos fuentes que no hayan sido modificados.

El ejemplo con el que trato de explicar este concepto en el artículo que dediqué a Rake es el siguiente:

file 'el-documento.odt'

file 'el-documento.pdf' => ['el-documento.odt'] do
  sh "lowriter --convert-to pdf el-documento.odt"
end

Sabemos que, a menos que modifiquemos el archivo el-documento.odt o que el archivo el-documento.pdf no exista, la tarea el-documento.pdf no será ejecutada (lo que nos ahorra tiempo).

Pero algo no muy conocido es el hecho de que Rake hace esta verificación SIEMPRE antes de ejecutar cualquier tipo de tarea, sea creada con el método file, con el método task o como sea que haya sido creada la tarea.

Lo que sucede es que cuando una tarea es creada convencionalmente (por ejemplo, con el método task) esta tarea siempre necesita ejecutarse.

Por el contrario, cuando una tarea es creada con el método file, ella es una tarea especial que necesita ser ejecutada según las fechas de última modificación del archivo correspondiente y el de los archivos asociados a las tareas de las cuales depende.

En cualquier caso, Rake le pregunta a la tarea misma si necesita ser ejecutada. Para ello invoca el método needed? de la tarea.

Podemos verificar esto desde la consola de Ruby. Supongamos que el-documento.pdf está recién creado y ejecutemos lo siguiente en la consola de Ruby:

> require 'rake'
> load 'Rakefile'
> Rake::Task['el-documento.pdf'].needed?
false

Como esperábamos, la tarea el-documento.pdf no necesita ser ejecutada (porque el PDF está recién creado). Pero si modificamos el archivo el-documento.odt y volvemos a preguntar:

> Rake::Task['el-documento.pdf'].needed?
true

Rake considera que la tarea el-documento.pdf necesita ser ejecutada.

Cómo hacer que Rake omita cualquier tipo de tarea con condiciones arbitrarias

¿Podemos aprovechar este mecanismo para omitir tareas por otras razones que no sean la fecha de última modificación de algún archivo?

Sí. Gracias al dinamismo de Ruby, es muy sencillo. Sólo debemos sobrescribir el método needed? de las tareas a precondicionar.

Un ejemplo muy sencillo sería definir tareas que debería ejecutarse sí y sólo si una variable de entorno está definida.

El siguiente archivo Rakefile ilustra la idea

def luz_verde?
  !!ENV['LUZ_VERDE']
end

task :una_tarea do
  puts "Una tarea dice: ¡Hola!"
end
def (Rake::Task[:una_tarea]).needed?
  luz_verde?
end

Con esta tarea así definida, desde la consola del sistema podemos pedirle a Rake que ejecute la tarea una_tarea, pero a menos que la variable de entorno LUZ_VERDE esté definida, sus acciones no se van a ejecutar:

$ rake una_tarea
# Nada pasa
$ LUZ_VERDE= rake una_tarea
Una tarea dice: ¡Hola!

Limitar una tarea con variables de entorno podría ser útil para proteger sistemas de tareas que sólo deberían ejecutarse en ciertos entornos.

Por ejemplo, algunas tareas de prueba que modifiquen bases de datos pueden ser peligrosas en entornos de producción.

Un caso de uso donde estoy utilizando esta técnica es en la gestión de contenedores Linux (LXC).

Una tarea que «lance» un contenedor puede verificar primero si existe o no dicho contenedor.

Igualmente una tarea que «elimine» un contenedor, puede verificar si existe antes de intentarlo.

Refinamiento

En vez de modificar cada tarea una y otra vez, podemos crear una clase especializada de Rake::Task que redefina el método needed?

Rake mismo tiene varias especializaciones que pueden servir de inspiración:

  • FileTask: Es el tipo de tarea especial que crea file.
  • FileCreationTask: caso especial de FileTask.
  • MultiTask: Ejecuta las tareas de las que depende en forma paralela.
  • PackageTask: Crea tareas para empaquetar archivos (tar, zip, etc).
  • TestTask: Tarea especializada en ejecutar pruebas.

En particular, FileTask y FileCreationTask sobreescriben el método needed?.

Conclusiones

Gracias al dinamismo de Ruby, podemos explorar esta técnica modificando tareas específicas, y si percibimos un patrón podemos expresarlo en una clase especializada de Rake::Task.

Esta técnica me parece muy valiosa porque nos permite usar Rake para controlar la ejecución de las tareas basandonos en condiciones arbitrarias del estado de sistemas, no necesariamente relacionadas con archivos.

Ahora sabemos que podemos asociar una tarea con diferentes condiciones del sistema como el estado de algún contenedor de Linux o la presencia de variables de entorno.

Pero más importante aún, sabemos que podemos verificar cualquier condición, como objetos en bases de datos, usuarios, retos (palabras clave), fechas (días festivos por ejemplo), etc.

Tags: Rake Ruby es