Procesando archivos en background con Rails

Procesar archivos en paneles de administración es un bajón, sobre todo cuando son cada vez más grande y cada vez más. En uno de nuestros proyectos tenemos ya unos cinco archivos CSV diferentes a procesar donde cada uno cumple alguna función específica. Algunos de estos archivos superan los 5 Mb por upload, por lo que se hizo necesario desacoplar el procesamiento del archivo del request que realiza el upload.

Para manejar procesos en background en rails hay muchas opciones buenas, algunas más complejas con colas de trabajo, workers distribuidos y otras cosas que nosotros no necesitamos. Lo importante para nosotros era encontrar alguno que permita :

  • Procesar en background (obvio :D)
  • Poder saber en que estado está el trabajo (% completado)
  • Al menos un proceso en background a la vez (si hay más que espere su turno, los uploads son semanales o mensuales por lo que no hay tanto solapamiento entre tareas como para justificar múltiples workers)

Y nada más. Buscando y buscando caí con BackgroundFu que cumplía con lo necesario.

Procesando los archivos

Como todos los archivos son de texto con algún separador (algunos ‘;’, otros tabs o comas) empezamos por lo básico y crear una clase base que maneje algunos parámetros comunes.

En BackgroundFu uno puede encolar cualquier clase, por lo que nuestro worker es simplemente una clase con un método process (que también es arbitrario el nombre, podría ser cualquier otro).

Como el archivo que sube el usuario está en un directorio temporal, antes de mandar al proceso en background en necesario guardarlo en algún lugar seguro, para poder saber que al momento de procesarlo va a seguir estando, para ello encapsulamos este comportamiento en un método especial para encolar.

Nuestra completa termina siendo algo como (más adelante veremos en detalle algunas cosas) :

class FileWorker
  include BackgroundFu::WorkerMonitoring

  attr_reader :total_lines

  def self.enqueue(file, *args)
    filename = "#{RAILS_ROOT}/tmp/#{self.name}-#{Time.now.to_i}"
    File.open(filename, "wb") { |f| f.write(file.read) }
    Job.enqueue!(self.name, :'process', filename, *args).id
  end

  def process(filename, *args)
    @lines = 0
    @total_lines = `wc -l #{filename}`.to_i
    record_progress(0)

    result = real_process(filename, *args)

    record_progress(100)
    File.unlink(filename)

    result
  end

  protected
  def line_processed!
    @lines += 1

    update(@lines)
  end

  def update(lines)
    record_progress(lines, @total_lines)
  end
end

Veamos un poco más en detalle algunas partes.

Encolado

  def self.enqueue(file, *args)
    filename = "#{RAILS_ROOT}/tmp/#{self.name}-#{Time.now.to_i}"
    File.open(filename, "wb") { |f| f.write(file.read) }
    Job.enqueue!(self.name, :'process', filename, *args).id
  end

Este método es quien guarda en un lugar seguro el archivo. Utilizando el nombre de la clase actual (recuerden que este método está en una clase base, de donde luego vamos a heredar) y un timestamp por las dudas que se lleguen a hacer 2 uploads (cosa que en la práctica no pasa, pero por las dudas está).

Luego de guardar el archivo se encola el trabajo pasándole el archivo (el path completo) y cualquier otro argumento que se quiera.

Esto hará que luego el daemon de background instancia la clase ‘self.name’ y llame al método process pasandole nuestros parámetros.

Procesamiento

El método genérico de procesamiento hace uso de un método que la clase base no tiene ‘real_process’ que es el método que las clases hijas definarán para realizar el trabajo final :

  def process(filename, *args)
    @lines = 0
    @total_lines = `wc -l #{filename}`.to_i
    record_progress(0)

    result = real_process(filename, *args)

    record_progress(100)
    File.unlink(filename)

    result
  end

En este caso el método se encarga de saber cual es el total de lineas (de una manera tal vez no muy linda, pero que anda :P) y borrar el archivo al terminar el proceso.

Un worker real

Para que esto tenga sentido, debemos crear un worker que haga realmente algo, entonces por ejemplo podríamos tener un worker que sume puntos a los usuarios desde un archivo :

class UserPointProcessor  "t").each do |row|
         # Hacer algo con row

         line_processed!
      end
    end

    "Podemos retornar algun valor o status de exito"
  end
end

De esta forma podemos ahora en algún controller hacer el encolado del trabajo :

  def add_points
    if request.post?
      redirect_to admin_job_path(UserPointProcessor.enqueue(params[:file])) and return
    end

    render 'file_upload'
  end

En este caso el template file_upload es un form con un file field para hacer el upload, sin ningún otro campo. La ruta admin_job_path nos la provee directamente BackgroundFu, generándonos también las vistas con AJAX para actualizar el estado, una linda barra de progreso que nos muestra el tiempo estimado para terminar entre otros datos útiles (o no tanto :D).

Take A Photo – Demo

Hace unos meses publiqué un plugin para tomar fotos instantáneas desde una página web, usando Flash y un plugin para Ruby on Rails.

Hoy me puse un rato a jugar y armé un demo donde se pueden tomar fotos (ojo que todo es público :D) : TAP.

Las fotos se borran una vez por día y acepta un máximo de 50 fotos por día (para evitar que me llenen el disco). Tengo algunos TODOs que iré viendo de resolver en los tiempos libres (como cambiar el tamaño de la foto, recortar, aplicar efectos, etc).

Necesita Flash 9 o superior y una web cam para que tenga sentido :). Todavía sigo peleando con el Flash player de Linux y la camarita, sorry :S.

Les recomiendo verlo en Firefox, no me gasté en ver si en IE el CSS no se rompe y no creo que lo haga nunca.

Take A Photo – Fotos instantáneas desde tu web

En la última semana estuve trabajando en agregar nuevas funcionalidades a un sitio de un cliente y entre los pedidos estaba una especie de Fotoblog para los usuarios (y bue, hay que pagar las cuentas a fin de mes :P).

La cosa salió rápido, usando Paperclip que guarda las imágenes que se suben, se muestra en orden, etc, nada del otro mundo. Pero hablando con el cliente surgió la idea de hacer que el usuario se pueda tomar una foto directamente desde la web usando su webcam, así que después de decir “si, se debe poder hacer” mentalmente me salió un “doh!, que dije!“. Lo último que se dijo en esa reunión fue “Lo quiero” :D.

Ya había visto juegos flash que utilizan la webcam así que empecé por ahí, para ver como sacaban un frame del video, lo cual era muy fácil. Lo siguiente era serializarla. A falta de algo mejor hice un serializador de imágenes muy pedorro, pero que anda (aunque es lento), que envía por POST la información de cada pixel.

La parte de Ruby fue fácil y decidí encapsularla en un plugin de Rails para poder reutilizarlo luego o por si a alguien más le sirve :). Además se puede integrar con Paperclip o AttachmentFu para hacer la persistencia de la imagen en donde sea.

La única parte que usa Flash es el capturador de la cámara, porque otra no quedaba, pero luego el botón para tomar la foto y los eventos se manejan todo por Javascript.

El plugin tiene varios TODOs, pero por si a alguien le sirve está en github.

Update : A falta de demo, algunas de las fotos tomadas con la aplicación de ejemplo que hice hoy durante Locos x rails.

Refactoring de “Fat Methods” – Episodio 4

En la última entrega habíamos logrado separar las notificaciones del controlador. Una de las cosas que todavía me seguían molestando eran los métodos donde obtengo las direcciones de email, como por ejemplo :

@notifications = @match.players.select {|p| p.user.notify_new_match_comment? }.collect {|p| p.user.email}

Lo que busca esto es obtener los emails de los usuarios para notificar ante un evento dado (un nuevo partido creado, un nuevo comentario, una petición de amistad, etc). Me molesta principalmente porque se ven horribles mis controllers :).

La respuesta que motiva este episodio llegó por casualidad. Intentando solucionar un bug que tenía en uno de los plugins que uso (has_many_friends, que es feo pero cómodo para un proyecto como este de prueba) vi algo que no conocía : las asociaciones (has_many, has_one, etc) aceptan un bloque donde podemos agregar funcionalidad extra.

Entonces, por ejemplo, si yo quisiera que el ejemplo anterior pueda ser escrito como @match.players.notificables (si, el nombre no es de lo mejor, pero es a modo ilustrativo) simplemente debería agregar ese método como sigue :

class Match < ActiveRecord::Base
  has_many :players  do
    def notificables
      collect {|p| p.user.email if !p.user.email.blank? && p.user.notify_new_match_comment? }
    end
  end
end

# Ejemplo de uso
@notifications = @match.players.notificables

El método que agregamos trabaja directamente sobre la asociación, que en este caso es una colección de Player, por lo que el collect nos dará todos los emails de los usuarios que se anotaron al partido y tienen activa la opción de recibir notificaciones cuando alguien deja un comentario nuevo en ese partido.

Los métodos que agregamos pueden aceptar parámetros también, con lo cual podría aceptar un symbol con el tipo de permiso a verificar y tener todos los emails de los usuarios para un tipo de notificación dada. Ahora si se hace un refactor de las opciones de notificación de los usuarios tenemos centralizada la lógica en los modelos en lugar de tenerla en los controladores u observers.

Reunión Mensual de Ruby Argentina

Hoy anunciamos la próxima reunión mensual que se va a desarrollar la semana que viene. Cualquier interesado en dar alguna charla puede dejarme un comentario.

El día 29/11 se presentará un conjunto de charlas y se lanzará el ciclo mensual de conferencias sobre Ruby y Ruby on Rails y organizado por Argentina on Rails y Ruby Argentina.

El evento se llevará a cabo entre las 11 y las 16hs, con un break para almorzar sobre las 13hs. Al momento contamos con la presencia de Luis Lavena, actual mainteiner del one-click installer de Ruby para Windows, Pedro Vistinin de Argentina on Rails. A medida que tengamos confirmación de los demás oradores la iremos comunicando.

Luis hablará del rake-compiler y draco
rake-compiler es una serie de tareas de rake que simplifican la compilacion de tanto extensiones como gemas en Ruby, cosa que hasta ahora viene bastante descuidada y caoticamente un desastre entre developers. Cositas lindas para que no tengan mas excusas de no hacer cosas para Windows…

Pedro Visintin hablará de como hacerse freelance (trabajando con rails
🙂 ) y no morir en el intento.

Si estás interesado en dar alguna charla referida a Ruby o Rails, mandá un mail  que veremos como proveerte un espacio en ésta o próximas ediciones del evento.

Lugar
La cita es en la Facultad de Ingeniería de la UBA, Paseo Colón 850 en el Aula 402, 4to Piso. Capital Federal

La entrada es libre y gratuita y no requiere registración previa.

Además, CaFeLug sorteará 2 libros de Ruby/Rails de la editorial O’Reilly entre los asistentes.

Delegando atributos de ActiveRecord

El otro día tratando de mejorar un modelo de una aplicación me topé con el problema de tener muchos atributos virtuales para poder acceder a atributos de un objeto relacionado y no caer en lo siguiente :

class Player < ActiveRecord::Base
  belongs_to :user

  def name
    user.name
  end

  def email
    user.email
  end

  # ... y varios más
end

Esto no lo puedo evitar mucho ya que Player es una clase intermedia en una relación de N-M entre los usuarios y los partidos, y además contiene información necesaria para la lógica del modelo.

Tampoco era muy feliz tener por todos lados Message Chains:

@player.user.name
@player.user.email
# etc ....

Una de las recomendaciones que se usan en estos casos es Hide Delegate para ocultarle al cliente de donde sale el dato realmente. Cabe aclarar que no siempre son un problema las llamadas encadenadas.

Recordando el anuncio de Rails 2.2 noté que al final hablaba de un delegate que tenía un nuevo feature. Buscando un rato por google encontré este post donde hablaba de un método delegate para hacer justamente esto que yo quería.

Lo extraño, que también menciona el autor es que no está documentado en la API oficial de Rails aunque mirando el código veo que explica como usarla. Explícitamente dice :

Provides a delegate class method to easily expose contained objects’ methods as your own

En la documentación también aclara que es útil tanto para atributos propios como para asociaciones entre diferentes instancias de ActiveRecord. La realidad es que por cómo está implementada funciona para cualquier objeto ruby que se nos ocurra.

El ejemplo con el que empezamos el post quedaría resumido a :

class Player  :user
end

Quedando mucho más corto el código, por lo tanto más fácil de mantener.

Tomando prestado los ejemplos del otro blog, también podemos hacer cosas con atributos que no son asociaciones, como por ejemplo una fecha :

# Forma abreviada
class Content  :published_at
end

# Forma desglosada
class Content  :published_at
  delegate :month, :to => :published_at
  delegate :day, :to => :published_at
end

# Podemos escribir
@content.year

# en lugar de
@content.published_at.year

Lo que viene en Rails 2.2

Hace poco se anunció el RC1 de lo que será la versión 2.2 de Ruby on Rails. Si bien la fecha de salida es “cuando esté lista”, ya se pueden utilizar para aquellos que gusten vivir “on the edge”.

Para aquellos que quieran un rejunte completo de todo lo que se viene pueden ver el post : What’s New in Edge Rails: Rails 2.2 Released – Summary of Feature.

Yo acá voy a comentar solo lo que tuve la oportunidad de probar y que me resultó útil.

I18n

La verdad es que es una buena noticia, no para mi porque hago todo para un solo idioma :P, pero algún día puede resultar útil. Lo único que a mi me deja con sabor a poco es la forma en que se hace que no me termina de gustar. Antes que nada quiero aclarar, porque ya vi la pregunta en un par de listas de correo, que esto localiza textos, no el contenido de una instancia de ActiveRecord. Si uno quiere que el contenido que cargan los usuarios sea traducido, es otra historia :).

Las traducciones se escriben en ruby o en archivos YAML, con “hashes” que pueden estar anidados (como si fueran “namespaces“) :

# lib/locale/en-US.rb
{ 'en-US' => {
  :hello_world => "Hello World",
  :hello_flash => "Hello Flash"
}}
 
# lib/locale/pirate.rb
{ 'pirate' => {
  :hello_world => "Ahoy World",
  :hello_flash => "Ahoy Flash"
}}
 

Y luego en las vistas (o mailers o donde sea) en lugar de poner el texto se ingresa el symbol asociado al texto que cargamos en las traducciones, por ejemplo :

Mi problema particular con este método es que estoy mucho más acostumbrado a cómo se hace con gettext (que no necesariamente es la mejor forma) donde se ponen todos los textos en inglés en la aplicación y después se escriben las traducciones.

Hay un demo online acá, que en estos momentos está caído, espero que para cuando lean esto ya este funcionando de nuevo.

Join Tables Conditions

Esto es algo que realmente hacía falta. Vamos a ver la mejora con un ejemplo. Supongamos que tenemos los siguientes modelos :

class Article < ActiveRecord::Base
  belongs_to :user
end

class User < ActiveRecord::Base
  has_many :articles
end

y queremos obtener todos los usuarios que tengan al menos un artículo publicado. Para eso deberíamos hacer :

User.find(:all, :joins => :article,  :conditions => ["articles.published = ?", true])

Con la nueva sintaxis es posible especificar este tipo de queries de una manera más amena como sigue :

User.find(:all, :joins => :article, :conditions => { :articles => { :published => true } })

ActionMailer Layouts

Algo que si me tocó vivir es tener varios emails HTML con un mismo formato y que cambiaba el contenido (Tu amigo te invitó, Tu nueva clave es, Ganaste un premio, etc). Con el mailer actual casi que hay que hacer un copy & paste del marco y despuer cambiar para cada tipo de email la lógica de qué se muestra.

En rails 2.2 ahora vamos a tener layouts como los tenemos en ActionView, de manera de tenerlo una vez y si arreglamos o cambiamos algo cambia para todos.

Memoization

Es muy frecuente que uno agregue lógica simple de caching en los modelos para las variables de instancia como se ve en el siguiente ejemplo :

  class User < ActiveRecord::Base
    def full_name
      @full_name ||= "#{first_name} #{last_name}"
    end
  end

Esto lo hacemos para evitar el overhead de crear el full_name si lo llegamos a usar varias veces en una misma vista, de manera que el string se crea en la primer llamada y en las siguientes solamente lo retorna.

Los puristas dicen que en realidad está mal, porque se está responsabilizando al método de algo que en realidad no debería importarle : la política de caching. ¿Suena exagerado? Seguramente :).

Para solucionar esto se agregó el método memoize que nos permite a agregar a un método común este tipo de lógica de manera separada :

  class User < ActiveRecord::Base
    def full_name
      "#{first_name} #{last_name}"
    end

    memoize :full_name
  end

Esto se encarga de que cuando llamemos a @user.full_name se comporte de la misma manera que el primer ejemplo, sin tener que modificar el método. También nos permite saltarnos el “cache”, por ejemplo si en la lógica acabamos de cambiar el nombre de pila, debemos forzar para que el nombre completo cambie :

   @user = User.new :first_name => 'Test', :last_name => 'Test'
   @user.full_name # => Test Test
   @user.first_name = 'Do'
   @user.full_name # => Test Test
   @user.full_name(true) # => Do Test

Y más

Hay varios cambios más, como Thread Safety, ETag, partial GETs y demás que a mi por ahora no me interesan y por eso no me entiendo mucho más. Pueden consultar el anuncio oficial para ver de que se tratan o cómo pueden hacerle la vida más fácil :).