Rails: Si necesitas callbacks, workers y estás pensando en arrays de objetos……..aquí te dejo las claves para entenderlo!


rubyonrailsHe estado trabajando en un proyecto express dentro de ASPgems, que consiste en desarrollar una idea y el compromiso de entregarlo en producción en una semana. No sé si otras empresas disponen de esta oportunidad en la que poder participar, pero yo me apunté sin dudarlo, es una experiencia muy buena, ya que te enfrentas a situaciones que en ocasiones los tienes resueltos y este tipo de reto, hace que vuelvas a tener la alerta activa. Os voy a contar en que consiste el proyecto para poder entrar en situación y entender de lo que intento explicar y las soluciones que he aplicado.

La aplicación consiste en saber que gemas está utilizando cada proyecto que se de de alta en la aplicación, por lo que debe disponer de una interfaz de usuario, para dar de alta un nombre de proyecto, una URL del repositorio para poder saber en su Gemfile las gemas que utiliza y leer en RubyGems  más información sobre ellas.

Según estaba planteando los retos de los puntos claves de la aplicación, lo primero que debía solucionar es, cómo interactúa esa interfaz de usuario y el tiempo de proceso que se lleva el disponer de toda la información de lectura del Gemfile, así como el reto en tiempo de ir a buscar cada una de las gemas a Rubygems y conseguir la información adicional de la descripción y el resumen. Esto plantea dos tipos de accesos externos que se necesitan para obtener información, uno al repositorio del proyecto y el otro a Rubygems y eso lleva un coste que hay que medir y tratar.

¿Cómo extraemos los procesos de una forma ligada e independiente al mimo tiempo? aquí es donde entran en juego varios protagonistas, los callbacks y los workers para hacer una combinación perfecta y que puedan hacerlo bien y no enmascarar algunos problemas que podemos encontrarnos. Lo más importante es en este caso es conocer bien la entrada en escena del callback que vamos a utilizar, es decir, tenemos que dar de alta un proyecto y cuando lo tengamos, debemos seguir operando para conseguir el resto de información para finalizar el proceso de “Alta de proyecto”. Las relaciones de nuestros modelos también es un factor a considerar en el cómo vamos a trabajar, en nuestro caso tenemos una relación has_many through belongs_to con Project (proyectos), Stone (gemas de cada proyecto) y Assignment la encargada de unirnos ambos modelos con belongs_to para Project y Stone.

Entonces ¿qué callbacks podríamos utilizar para esta operación? si pensamos en un after_create, after_update o un after_save no son los callbacks más adecuados para la operativa con el worker ya que obtendríamos resultados poco acertados dada la relación en este caso con los modelos. No podemos dar por terminada la operativa hasta que no tengamos todo nuestro ciclo completo, el proyecto y las gemas asociadas al proyecto. La clase GemfileParser se encarga a través del método parse, de leer el fichero Gemfile con una operativa IO y crea las gemas o si existen, nos proporciona el objeto encontrado con la siguiente línea de código:

def parse
       ………………………………….more code
       gems = []
        IO.popen(url).each do |l|
              l=l.split(‘ ‘, 3)
              if l[0].start_with?(“gem”)
                  gem_name = l[1].split(/[“‘]/)[1]
                  gems << Stone.where(name: gem_name).first_or_initialize
              end
       end
      gems
end

El array gems, vamos obteniendo los objetos creados o los encontrados, es decir, tenemos un array de objetos gemas para después seguir tratando con ellas en nuestro worker y actualizar la información, en un segundo plano. En nuestro modelo Project tenemos un método que está relacionado con nuestro callback  after_createafter_update o un after_save, una vez creado nuestro proyecto, que aún no le hemos puesto el más adecuado, pero antes un apunte más. Fíjate en el siguiente método que tenemos ligado a un callback que diremos su tipo un poco más adelante:

def assign_gems
      gems = GemfileParser.new(self).parse
     self.stones = gems
     FetchGemInfoWorker.perform_async(gems)
end

Lo que decíamos antes es que el método parse se encargaba de hacer la tarea de crear las nuevas gemas o de obtener las existentes e incorporarlas en el array de objetos gems, que es lo que nos devuelve dicho método con la llamada gems = GemfileParser.new(self).parse. Ahora el segundo problema que nos encontramos es que si pasamos directamente el array de objetos al worker, que es lo más normal que pensemos que es lo correcto, ya que trabajos con objetos, esto nos trae el problema en conjunto del callback y el array de objetos en el worker. Cuando llega el array de objetos al worker podemos encontrarnos que con un initialize, un objeto aún no creado o que intentemos trabajar sobre el  objeto, puede que no sea el mismo y seguramente nos encontremos con objetos aún no creados en la base de datos, esto significa que se nos pueden crear objetos por duplicado. Por tanto es importante tener un after_commit para asegurarnos que ya están en la base de datos guardados (Insolation de ACID)  y segundo es que le pasemos al worker datos, lo hagamos de tal forma que le psaemos los id para buscarlos o ya serializados, pero no objetos, ya que no es capaz de identificarlos. Por tanto una solución es preparar antes los datos de las gemas a tratar gems = self.stones.map(&:name) y se lo pasamos al worker:

def assign_gems
      gems = GemfileParser.new(self).parse
     self.stones = gems
     gems = self.stones.map(&:name)
     FetchGemInfoWorker.perform_async(gems)
end

En este caso antes de pasar a FetchGemInfoWorker el array de objetos, creamos un array con los nombre que vamos a tratar dentro del worker y así buscamos cada una de ellas y actualizamos la información, pero también aplicamos un after_commit como callback para asegurarnos de que todo va a funcionar correctamente. Por tanto dentro del worker tenemos el code siguiente:

def perform(gems)

     gems.each do |gem|
          gem_info = RemoteGem.find(gem.to_s)  => Clase que se encarga de ir a buscar a RubyGems la información de cada gema y actualizar description y summary.
          unless gem_info.nil?

       stone = Stone.find_by name: gem.to_s

                 stone.description = gem_info.description
                 stone.summary = gem_info.summary
                 stone.save
          end
     end
end

Como resumen final recuerda tener presente un after_commit para tener los objetos creados y después a la hora de tratarlos en un worker debes pasar los IDs para buscarlos dentro y tratarlos.

NOTA: gracias a eLafo por darme la oportunidad de trabajar en el proyecto express, también agradecer a supercoco9 y acirugeda por participar en una charla por el irc y aclarar algunos puntos claves que ha dado para escribir este post, Gracias!!.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s