viernes, 20 de febrero de 2009

ruby "retry" más elegante utilizando module_eval

Muchas veces necesitamos hacer un retry de un bloque de código n veces después de recibir una excepción. Por ejemplo podríamos recibir un "execution expired" tratando de conectarse a un mail server , un "Connection closed" transfiriendo un archivo por ftp, etc.

Como hacerlo al estilo ruby?

Si queremos hacer un retry de 2 o 3 veces es tan simple como esto:


retries = 2
begin
raise "error"
rescue => e
puts e.message
retry if (retries-=1)>0
end


La sentencia raise simula la detección del error en el ejemplo, ya que llama la excepción (manejada por el Objecto Exception) enviando un mensaje llamado "error".

El resultado es:


error
error


Cae al bloque rescue que maneja el error y imprime el error ("puts e.message").

Como hacerlo más elegante? :)

Me gustaría poder hacerlo así:


ok = retry_two do
raise 'error'
end
puts ok


con el siguiente resultado:


error
error
false


Es decir, ejecutar el bloque haciendo un retry de 2 escrito en texto que reciba un bloque y que devuelta si este fue ejecutado correctamente (asignándolo a la variable ok).

Este código permite hacer eso:


require 'rubygems'
require 'linguistics'
Linguistics::use(:en)
module FortyRetry
40.times do |n|
module_eval %Q{
def retry_#{(n+1).en.numwords.gsub(' ','_').gsub('-','_')}
x = #{n} + 1
ok = true
begin
yield
rescue => e
puts e.message
retry if (x-=1)>0
ok = false
end
ok
end
}
end
end


Esta implementación utiliza "module_eval" que evalúa un string o bloque en el contexto de un modulo (utilicé module_eval en ves de define_method por ciertos problemas con los bloques explicados aquí). En este caso definí un modulo llamado "FortyRetry" porque esta genera 40 retries en el siguiente estilo:

retry_numero_en_letras do
...
...
end

hagamosle un "puts FortyRetry.methods" para verlos:


puts FortyRetry.methods.sort


....
public_method_defined?
public_methods
respond_to?
retry_eight
retry_eighteen
retry_eleven
retry_fifteen
retry_five
retry_forty
retry_four
retry_fourteen
retry_nine
retry_nineteen
retry_one
retry_seven
retry_seventeen
retry_six
retry_sixteen
retry_ten
retry_thirteen
retry_thirty
retry_thirty_eight
retry_thirty_five
retry_thirty_four
retry_thirty_nine
retry_thirty_one
retry_thirty_seven
retry_thirty_six
retry_thirty_three
retry_thirty_two
retry_three
retry_twelve
retry_twenty
retry_twenty_eight
retry_twenty_five
retry_twenty_four
retry_twenty_nine
retry_twenty_one
retry_twenty_seven
retry_twenty_six
retry_twenty_three
retry_twenty_two
retry_two
send
singleton_met......

La librería "linguistics" es la encargada de realizar la conversión de números a números-palabras.

Por ejemplo:

10.en.numwords => ten
28.en.numwords => twenty eight

entonces cuando hacemos:

module_eval %Q{
def retry_#{(n+1).en.numwords.gsub(' ','_').gsub('-','_')}
....

Crea los métodos en texto convirtiendo los números a texto-números.

Ya podemos hacerlo elegantemente en nuestro código:


include FortyRetry
puts retry_seventeen{ raise 'error'}
puts retry_six{ raise 'error'}
puts retry_sixteen{ raise 'error'}
puts retry_ten{ raise 'error'}
puts retry_thirteen{ raise 'error'}


Con los siguientes resultados:


error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
error
error
error
false


Mucho más elegante sin duda :).