Gastón Ramos

"La mayoría de las gaviotas no se molesta en aprender sino las normas de vuelo más elementales: como ir y volver entre playa y comida. Para la mayoría de las gaviotas, no es volar lo que importa, sino comer. Para esta gaviota, sin embargo, no era comer lo que le importaba, sino volar. Más que nada en el mundo, Juan Salvador Gaviota amaba volar".

Category: rubygem

¿Cómo funciona RubyGems?

Desde hace un tiempo que estoy interesado en conocer un poco más en detalle como funciona rubygems, así que hoy decidí comenzar a aprender más sobre rubygems y cómo las gemas son cargadas en memoria.

Bueno, comencemos:
Entro al intérprete interactivo de ruby e intento requerir la bioblioteca activemodel:


ruby-1.9.2-p136 :001 > Gem.loaded_specs
 => {}
ruby-1.9.2-p136 :002 > require 'active_model'
 => true 
ruby-1.9.2-p136 :003 > Gem.loaded_specs
 => {"activemodel"=>#<Gem::Specification name=activemodel version=3.0.4>, 
"activesupport"=>#<Gem::Specification name=activesupport version=3.0.4>, 
"builder"=>#<Gem::Specification name=builder version=2.1.2>, 
"i18n"=>#<Gem::Specification name=i18n version=0.5.0>}

Cómo podemos observar aparecen varias gemas nuevas cargadas, active_model y sus dependencias, pero ¿cómo es que esto se lleva cabo?

Bien si nos fijamos en /lib/rubygems.rb en el repo de rubygems al final de todo hace esto:


require 'rubygems/custom_require' unless Gem::GEM_PRELUDE_SUCKAGE

y si vamos al archivo lib/rubygems/custom_require.rb vemos que lo hace es redefinir el método require de Kernel, y activa la gema de esta forma:


Gem.try_activate(path)

podemos probar esto directamente en el intérprete:


ruby-1.9.2-p136 > Gem.try_activate 'active_resource'
=> true
ruby-1.9.2-p136 :007 > Gem.loaded_specs['activeresource']
 => #<Gem::Specification name=activeresource version=3.0.4> 


Entonces vamos de nuevo al archivo rubygems.rb y miramos como es que funciona Gem.try_activate:

Lo primero que hace es buscar la gema que estamos intentando requerir, y lo hace así:


 spec = Gem.searcher.find path

Gem.searcher es una instancia de Gem::GemPathSearcher entre otras cosas tiene la lista de todas las gemspecs instaldas para después poder hacér búsqueda con Gem::GemPathSearcher#find. Entonces siguiendo con Gem.try_activate, si encuentra alguna coincidencia ejecuta

Gem.activate spec.name, "= #{spec.version}"

como podemos ver saca la versión de la spec encontrada (que es la más alta instalada), entonces ahora vamos a ver

Gem.activate

Éste método retorna true si la gema fué activada, false si ya está cargada o una excepción en cualquier otro caso (no se pudo cargar por algún motivo). La manera en que activa la gema es agregando el path de la misma al $LOAD_PATH de ruby.


 def self.activate(gem, *requirements)
   if requirements.last.is_a?(Hash)
      options = requirements.pop
   else
      options = {}
   end
  
   # Esta línea parsea los requerimientos que vienen como argumento
   # y si no viene ninguno pone los defaults, los requerimientos no son
   # otra cosa que restricciones de la versión de la gema,
   # el requerimiento por defecto es >= 0

   requirements = Gem::Requirement.default if requirements.empty?
   
   # Básicamente Gem::Dependency es un contenedor de 
   # Gem name, requirement

   dep = Gem::Dependency.new(gem, requirements)

   
   sources = options[:sources] || []

   # creo que esta línea require especial antención 
   # (veremos los detalles más adelante) pero por ahora nos 
   # alcanza con saber que Gem._unresolved crea una lista 
   # de objetos Gem::DependencyList (que primero está vacía) 
   # de dependecias sin resolver
   
   matches = _unresolved.find_all { |spec| spec.satisfies_requirement? dep }

   if matches.empty? then
      matches = Gem.source_index.find_name(dep.name, dep.requirement)

      # Si no ecuentra la gema buscando con el nombre y los 
      # requerimientos reporta el error
      report_activate_error(dep) if matches.empty?
      
      # Si la gema ya está cargada (cualquier versión)
      if @loaded_specs[dep.name] then
        # This gem is already loaded.  If the currently loaded gem is not in the
        # list of candidate gems, then we have a version conflict.
        existing_spec = @loaded_specs[dep.name]
        
        # Pregunta si la gema que está cargada es la misma versión 
        # que la  quiero activar
        unless matches.any? { |spec| spec.version == existing_spec.version } then
          sources_message = sources.map { |spec| spec.full_name }
          stack_message = @loaded_stacks[dep.name].map { |spec| spec.full_name }

          msg = "can't activate #{dep} for #{sources_message.inspect}, "
          msg << "already activated #{existing_spec.full_name} for "
          msg << "#{stack_message.inspect}"

          e = Gem::LoadError.new msg
          e.name = dep.name
          e.requirement = dep.requirement

          raise e
        end
        
        # La gema que quiero cargar ya está cargada
        return false
      end
    end
    
    # La gema no está cargada entonces activa la última versión
    spec = matches.last

    conf = spec.conflicts
    unless conf.empty? then
      why = conf.map { |k,v| "#{k} depends on #{v.join(", ")}" }.join ", "

      raise LoadError, "Unable to activate #{spec.full_name}, but #{why}"
    end

    return false if spec.loaded?

    spec.loaded = true
    @loaded_specs[spec.name]  = spec
    @loaded_stacks[spec.name] = sources.dup

    spec.runtime_dependencies.each do |spec_dep|
      next if Gem.loaded_specs.include? spec_dep.name
      specs = Gem.source_index.search spec_dep, true

      _unresolved.add(*specs)
    end

    current = Hash.new { |h, name| h[name] = Gem::Dependency.new name }
    Gem.loaded_specs.each do |n,s|
      s.runtime_dependencies.each do |s_dep|
        current[s_dep.name] = current[s_dep.name].merge s_dep
      end
    end

    _unresolved.remove_specs_unsatisfied_by current

    require_paths = spec.require_paths.map do |path|
      File.join spec.full_gem_path, path
    end

    # gem directories must come after -I and ENV['RUBYLIB']
    insert_index = load_path_insert_index

    if insert_index then
      # gem directories must come after -I and ENV['RUBYLIB']
      $LOAD_PATH.insert(insert_index, *require_paths)
    else
      # we are probably testing in core, -I and RUBYLIB don't apply
      $LOAD_PATH.unshift(*require_paths)
    end

    return true
  end


Si alguien encuentra algún en error en mi seguimiento hasta acá le pido que haga un comentario en este post, Continuará…

Advertisements

Gems mirror local

Dado que tengo un enlace a internet que no es lo que uno quisiera tener, cada vez que quiero instalar una gema, me demora varios minutos, lo cual es bastante molesto cuando uno está trabajando, por esto decidí poner mi propio gems mirror local, pasemos a la acción:

primero tenemos que editar el archivo de configuración para nuestro gem mirror:

emacs -nw ~/.gemmirrorrc y ponemos el siguiente contenido:

--- 
- to: /home/gemsmirror/rubygems.mirro 
  from: http://gems.rubyforge.org

noten que yo voy a poner mi mirror en la carpeta /home/gemsmirror/rubygems.mirror pero uds la pueden poner en cualquiera que quieran.

luego de esto ejecutamos el comando

gem mirror

y después un largo pero largo rato (a mí esto me llevó como un mes con interrupciones, son como 16GB de gemas) se baja todas las gemas del mirror remoto. Una vez hecho y terminado esto tenemos que configurar nuestro servidor web para que publique el mirror, yo lo hice con apache, así sería:

emacs -nw /etc/apache2/sites-available/gemsmirror

y ponemos lo siguiente



        ServerName gems.localhost
        ServerAdmin root@gems.localhost
        DocumentRoot /home/gemsmirror/rubygems.mirror/
        
                Options Indexes FollowSymLinks MultiViews
                AllowOverride None
        

        
                Options Indexes FollowSymLinks MultiViews
                AllowOverride None
       #AuthName "Dev Access"
       # AuthType Basic
       # AuthBasicProvider file
        # AuthUserFile /opt/coolstack/apache2/conf/htpasswd.users
       # Require user gems
# user: gems
# pass: password
                Order allow,deny
                allow from all
        

        ErrorLog /var/log/apache2/error-gemsmirror.log
        LogLevel warn
        #CustomLog /var/log/apache2/access-gemsmirror.log combined
        ServerSignature Off


Lo puse sin autenticación por que yo solamente le doy un uso local pero si quieren publicarlo en internet sería razonable ponerle clave.

Ahora habilitamos el site de mirror

a2ensite gemsmirror

y agregamos el host:

emacs -nw /etc/hosts

ponemos lo siguiente:

....
######################################################################
# Gemsmirror local
127.0.0.1 gems.localhost

.....

Ahora recargamos el apache:

/etc/init.d/apache2 reload

y vamos al navegador y chequeamos que se vean el directorio de nuestro mirror en:

http://gems.localhost/

si todo está bien agregamos nuestro mirror local al gemrc:

 emacs -nw ~/.gemrc
--- 
:sources: 
- http://gems.localhost
#- http://gems.rubyforge.org/
#- http://gems.github.com
#- http://gemcutter.org/
#- http://gems.github.com
:bulk_threshold: 1000
:benchmark: false
:update_sources: true
:backtrace: false
:verbose: true

fijensé que yo comenté todos los demás mirrors, de esta manera sólo busca las gemas en el mirror local y al instalar o buscar cualquier cosa la velocidad es increíble :), ahora solo resta pner un cron para que actualice el mirror diariamente (por la noche) y listo.

Eso es todo, enjoy !

RubyGems bastante lento

Tengo un server con un micro AMD-K6(tm) 3D processor y 128 MB de RAM con un Debian GNU/Linux con ruby 1.8 RubyGems 0.9.4 mogrel y nginx 0.4.13-2, intentando hacer un gem update noté que se tomaba su tiempo, Updating installed gems…, luego de unas cuantas horas seguía en el mismo lugar, al parecer el problema está cuando cuando arma las lista de dependencias de las gemas, googleando un poco encontré esto: http://blog.segment7.net/articles/2007/10/13/rubygems-beta-0-9-4-5 una versión beta de rubygems que soluciona este problema, entonces pasé a instalarla:

gem update --system --source http://segment7.net/

Ahora funciona bastante mejor!!! al menos demora minutos y no horas como antes, como dice en el sitio este release mejora el manejo de memoria.