Archive for the 'Ruby on Rails' Category

Como preparar fixtures de forma rápida y sencilla

Vamos con un post rapidito. Llevo unos dias haciendo functional tests, y el tema de los fixtures, como mucha gente ya ha dicho, es un asco.

Para ello me he creado una tarea Rake sencilla que puede ayudaros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  desc "Prepare fixtures"
  task :prepare_fixtures => :environment do
 
    class FixturePreparer
      class << self
        def generate(name, collection)
          f = File.open("test/fixtures/#{name}.yml",'w+')
          collection_data = collection.map {|x| { (block_given? ? (yield x) : (x.respond_to?(:slug) ? x.slug : x.id)) => x.attributes }.to_yaml }
          collection_data.each {|item| f.puts item.sub(/.*\n/,'') }
          f.close
        end
      end
    end
 
    puts "Preparing Users"
    FixturePreparer.generate('users', User.find([1,2,3]))
 
    puts "Preparing Groups"
    FixturePreparer.generate('groups', Group.find([1,2,3,4]))
    FixturePreparer.generate('groups_users', GroupsUser.find(:all, :conditions => { :group_id => [1,2,3,4] }))
 
    puts "Preparing Discussions"
    discussions = Discussion.find(:all, :conditions => { :discutable_type => 'Group', :discutable_id => [1,2,3,4] })
    FixturePreparer.generate('discussions', discussions)
    FixturePreparer.generate('discussions_users', DiscussionsUser.all(:conditions => { :discussion_id => discussions.map(&:id) }) ) {|item| "#{item.user_id}-#{item.discussion_id}" }
    FixturePreparer.generate('discussion_categories', DiscussionCategory.all(:conditions => { :discussion_id => discussions.map(&:id) }) ) {|item| "#{item.discussion_id}-#{item.slug}" }
    FixturePreparer.generate('discussion_topics', DiscussionTopic.all(:conditions => { :discussion_id => discussions.map(&:id) }) )
    FixturePreparer.generate('discussion_posts', DiscussionPost.all(:conditions => { :discussion_id => discussions.map(&:id) }) )
 
    puts "Preparing Countries"
    FixturePreparer.generate('countries', Country.all)
 
  end

Creo que el código es auto-explicativo, pero hagamos un repaso rápido:
El tema importante es la classe FixturePreparer, al cual se le pasa un nombre y un array de ActiveRecords, opcionalmente se le puede pasar un bloque.

El nombre será el nombre del fichero a guardar, el array de ActiveRecords es la colección de registros que se quieren poner en el fixture, y por último, el bloque. El bloque es opcional y sirve para poner un identificador único a cada registro dentro del fixture, si no le pasamos ningún bloque, el identificador será el slug, y si no tiene slug, pues el id. En caso que le pasemos un bloque, evaluará el bloque para cada registro. Hay que tener en cuenta que el identificador de registro DEBE SER UNICO en cada yml.

De esta forma, puedes generarte un pequeño set de datos minimos a partir de tu db de desarrollo e ir actualizando los fixtures conforme actualizas tu base de datos.

El código es tan pequeño que no he creido necesario crear un repositorio en github para permitir forks, etc… pero si alguien esta interesado, lo creo!

Suerte con los tests. ;)

Editado: Xavier Noria, siempre en su linea de expresiones regulares varias, nos proporciona un parche para hacer mejor la clase FixturePreparer, gracias!

Ruby/Rails y sus cositas…

Llevo un buen rato perdiendo el tiempo por culpa de un “behaviour” de ruby/rails, pero después de mucho batallar por fin he encontrado lo que estaba pasando, y lo he solucionado.

Escribo esto para ver si san google me indexa el blog y desde aqui puedo ayudar un poco a los pobres desarrolladores web que se enfrenten al mismo problema que yo… Memory Leaks!!

Pero esta vez de los gordos, nada de mariconadas como el GetText 1.90 y su Mb por request perdido….

El tema tiene relación con activerecord, una asociación has_many :through junto con otra has_and_belongs_to_many y un callback. Vayamos a por el ejemplo práctico:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class User < ActiveRecord::Base
  has_many :groups_users
  has_many :groups, :through => :groups_users
  has_and_belongs_to_many :discussions
end
 
class Group < ActiveRecord::Base
  has_many :groups_users
  has_many :users, :through => :groups_users
  has_one :discussion
  before_create :create_discussion
end
 
class GroupsUser < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
  before_create :add_owner_to_discussion
 
  def add_user_to_discussion
    group.discussion.users << user
  end
 
end
 
class Discussion < ActiveRecord::Base
  belongs_to :group
  has_and_belongs_to_many :users
end

El tema está claro, un usuario puede pertenecer a n grupos, cada grupo tiene una discusión, y los usuarios pertenecen a estas discusiones.

Vayamos a la consola, primero creamos 3000 usuarios

1
>> 3000.times {|x| User.create }

Vale, ahora creemos un grupo

1
>> g = Group.create

Y como paso final…. le asignamos a este grupo todos los usuarios

1
>> User.all.each {|u| g.users << u}

PAM! Si lo has ejecutado de petará la maquina por exceso de memoria, en mi caso, 2 Gb de SWAP ocupadas en mi precioso MacBook Pro.

¿Cuál es el problema?

El memory leak está en el callback definido en GroupsUser. Al parecer en una asociación has_and_belongs_to_many, el método << devuelve el proxy (en nuestro caso users) y este proxy se queda en el limbo de ruby al finalizar la llamada al callback, provocando que para cada llamada al callback el proceso pierda bastante memoria (calculo que entre 500 kb y 1 Mb). ¿Cómo solucionarlo? Fácil, asigna una variable y despues nullificala para borrar de la memoria el proxy, en nuestro ejemplo:

1
2
3
4
  def add_user_to_discussion
    x = group.discussion.users << user
    x = nil
  end

That’s it, problema resuelto. Increible pero cierto. Atentos a vuestro código. ;)

Attachment_fu + S3 + Europa

Ya se que tengo pendiente un post sobre los controladores polimorficos en rails, pero hoy queria contar algo que creo es interesante.

Existe un plugin en Rails llamado attachment_fu, que permite de forma muy sencilla tener ficheros asociados a un modelo. Normalmente se utiliza para imagenes, ya que el propio plugin hace realmente sencilla la tarea de generar miniaturas (thumbnails) a diferentes tamaños para su posterior visualización.

Este plugin, además de permitir generar miniaturas y hacer senzillo el “upload” del fichero, permite guardar estos ficheros de distintas formas: en disco, en base de datos o en el famoso Amazon S3.

Lo recomendable es utilizar S3 por algunos motivos, entre ellos:

  • Espacio virtualmente ilimitado
  • Costes controlables y reducidos (pagas por uso y transferencia)
  • Despreocupate de los backups

Pero aqui empiezan los problemas, ya que attachment_fu utiliza el gem AWS::S3 para gestionar la relación con S3, y dicho gem tiene un pequeño bug que hace imposible subir contenido a un bucket europeo, solo funciona bien con los americanos. (El secreto es que el gem utiliza la API REST de Amazon, y siempre envia la cabecera “Host: s3.amazonaws.com”, cuando para los buckets europeos debe ser “nombre_bucket.s3.amazonaws.com”).

Otro aspecto negativo de attachment_fu es que solo te permite subir ficheros a un Bucket. Que pasa si quiero tener las fotos de los usuarios repartidas en distintos buckets? Pongamos un ejemplo práctico:

En moterus tenemos aspiración internacional, queremos tener usuarios de todo el mundo. Los usuarios pueden subir sus fotos, y queremos que las fotos se visualizen lo más rapido posible a todos los usuarios. Para que las fotos se visualizen rápido, es importante que estén en un servidor lo más cerca posible del usuario. Por este motivo Amazon tiene un datacenter en USA y otro en Europa. Tenemos que hacer que los usuarios americanos utilicen un bucket de USA y que los usuarios europeos utilizen un bucket de Europa.

Con el attachment_fu “estándar” esto no es posible. Pero por suerte existe GitHub, que nos permite hacer cosas como la que hemos hecho en VeSNe: un fork!

Ahi vamos. Si quieres utilizar distintos buckets de S3 en tus modelos utilizando attachment_fu o si deseas tener buckets europeos, utiliza nuestros forks en github:

AWS::S3: http://github.com/isaacfeliu/aws-s3

sudo gem install isaacfeliu-aws-s3 --source=http://gems.guithub.com

Attachment_fu: http://github.com/isaacfeliu/attachment_fu

script/plugin install git://github.com/isaacfeliu/attachment_fu.git

Para el AWS::S3 gem no hace falta nada en especial. Simplemente borrad el anterior si lo tuvierais (sudo gem uninstall aws-s3) y listo.

Para el attachment_fu hay algun cambio a destacar en el fichero config/amazon_s3.yml:

  • Ya no se acepta el parámetro “bucket_name”
  • Ya no se acepta el parámetro “server”
  • Hay un nuevo parámetro “buckets” donde se le pasa la lista de buckets con los que trabajar (separados por espacios), por ejemplo (fotos.moterus.es photos.moterus.com foto.moterus.it)
  • Hay un nuevo parámetro “use_vhosts” (true o false), para indicar si deseamos utilizar el nombre del bucket como dominio o se debe utilizar el dominio generico de amazon, es decir, si queremos que las urls sean del estilo: http://fotos.moterus.es/… o bien http://fotos.moterus.es.s3.amazonaws.com/… Para que nos funcione correctamente debemos tener en nuestro DNS un registro del tipo “fotos.moterus.es IN CNAME fotos.moterus.es.s3.amazonaws.com”

Además, en los modelos donde queramos trabajar con attachment_fu deberemos tener un campo en la tabla con el nombre “bucket_name” de tipo string, que es donde se guardará en qué bucket se encuentra su attachment. Y en el controlador que se encargue de crear el registro (Por ejemplo, photos_controller) deberemos pasarle el bucket en el metodo “create” del modelo, por ejemplo:

  def create
    Photo.create(params[:photo].merge(:bucket_name => 'fotos.moterus.es'))
  end

Y esto es todo! Te funcionará en buckets europeos y americanos, y podrás tener distintos buckets para cada modelo. Que mas quieres? Probad, probad, si todo funciona bien y no hay problemas, intentaremos que nos acepten el parche y lo implementen en el attachment_fu de verdad.

Dudas, comentarios?