Mot-clé - ruby

Fil des billets

dimanche 17 mai 2015

Rename a file before saving with Paperclip

I was recently trying to rename a file before having it saved by Paperclip. As it was not obivous to me, I would like to share the solution with you.

In the model file with the paperclip "definitions", let's add:

before_post_process :rename_file

and (in my case, the paperclip attachement name is "file")

def rename_file
  self.file.instance_write :file_name, function_to_change_a_string(file_file_name)
end

And it should work. Easy once found :-)

samedi 16 mai 2015

Unicorn vs Thin pour propulser une application Ruby on Rails, stress test pour choisir !

Unicorn et Thin sont deux serveurs pour Ruby assez fréquemment utilisés et cités dans la littérature informatique. A titre d'exemple, Unicorn est utilisé notamment chez Github: une bonne référence ! Thin est souvent cité pour sa robustesse et sa légèreté. Lequel choisir ?

Avant d'envisager le déploiement d'une application en production (prévision de charge : quelques centaines d'utilisateurs simultanément), j'ai souhaité les comparer.

Protocole de test

  • Linux Debian Jessie, Core M 5Y10, 4 Go de RAM, disque dur SSD ; les tests sont effectués en local sur la machine et le réseau n'intervient donc pas comme facteur dans le test.
  • Le stress test est réalisé avec Gatling (dont j'ai déjà décrit le fonctionnement ici) qui simule l'arrivée de 200 utilisateurs en 120 secondes sur l'application, chaque utilisateur exécute 8 requêtes sur l'application.
  • L'application Ruby on Rails (Rails 4.2.1, Ruby 2.2.2) est prête à l'emploi en environnement de production, les 2 gems Thin et Unicorn sont installés et fonctionnels.
  • Quand cela est nécessaire, Pound est utilisé comme "load balancer".
  • Thin est exécuté avec la commande :
thin -s 4 -e production -p 8080 start

ou, quand on le lance avec un seu

thin -e production -p 8080 start
  • Unicorn est exécuté par la commande :
unicorn -c config/unicorn.rb -E production -p 8080

avec pour contenu du fichier de config unicorn.rb une configuration très proche de celle proposée par Github sur cet article :

rails_root = "/path/to/app"
rails_env = ENV['RAILS_ENV'] || 'production'

# 4 workers, may be changed to 1 for the tests
worker_processes (rails_env == 'production' ? 4 : 1) 

# Load rails+app into the master before forking workers
# for super-fast worker spawn times
preload_app true

# Restart any workers that haven't responded in 30 seconds
timeout 30

before_fork do |server, worker|
  ##
  # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and
  # immediately start loading up a new version of itself (loaded with a new
  # version of our app). When this new Unicorn is completely loaded
  # it will begin spawning workers. The first worker spawned will check to
  # see if an .oldbin pidfile exists. If so, this means we've just booted up
  # a new Unicorn and need to tell the old one that it can now die. To do so
  # we send it a QUIT.
  # Using this method we get 0 downtime deploys.

  old_pid = rails_root + '/tmp/pids/unicorn.pid.oldbin'
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # Unicorn master loads the app then forks off workers - because of the way
  # Unix forking works, we need to make sure we aren't using any of the parent's
  # sockets, e.g. db connection
  ActiveRecord::Base.establish_connection
end

Les résultats

Match 1 : Unicorn vs Thin, avec un seul processus pour chacun

Quand un seul processus (un seul "worker" dans le vocable propre à Unicorn) est activé, l'arrivée ininterrompue de nouveaux utilisateurs charge considérablement l'application...

Voici les résultats du "stress test" : benchmark_1process.png

On observe que :

  • Thin parvient à servir tout le monde, mais au prix de temps de réponse assez longs
  • Les temps de réponse sont un petit plus courts avec Unicorn mais en revanche Unicorn échoue à servir un certain nombre d'utilisateurs - le résultat du second graphe montre que l'unique worker d'Unicorn arrête de répondre aux requêtes à plusieurs reprises et pendant des durées assez longues... je n'explique pas bien ce comportement... J'ai pensé à l'impact du paramètre 'timeout 30' mais je n'ai pas observé de différence lorsque j'ai porté la valeur du timeout à 300 (j'ai même observé une amplification du phénomène)...
  • Thin semble donc vainqueur sur ce match avec un seul processus... mais voyons vite avec plusieurs processus ce qui sera plus proche d'un déploiement réel !

Match 2 : Unicorn vs Thin, avec 4 processus pour chacun

Nous démarrons cette fois Unicorn avec 4 "workers" et 4 instances de Thin. Afin d'accéder de manière aléatoire aux différentes instances de Thin, nous utilisons cette fois Pound comme "load balancer". Pound a également été utilisé avec Unicorn pour rendre les conditions des tests équivalentes. Pound n'est toutefois "en théorie" pas nécessaire pour Unicorn qui répartit automatiquement les requêtes sur les différents "workers".

Place aux résultats : benchmark-4processus.png

On constate que :

  • Thin et Unicorn parviennent à servir tous les utilisateurs plus efficacement (normal, la multiplication des processus profite des plusieurs coeurs de la machine hôte)
  • Mais Unicorn est plus véloce que Thin : 97% des requêtes sont servies en moins de 800ms par Unicorn alors que seules 63% sont servies dans le même délai par Unicorn
  • On remarque également que les 4 processus d'Unicorn jugulent le flux des nouveaux utilisateurs avec au pic 24 utilisateurs en attente là où Thin faiblit peu à peu jusqu'à atteindre un pic à plus de 50 utilisateurs en attente dans l'application (pour rappel, dans le scénario testé, 200 utilisateurs se connectent régulièrement au cours des 120 secondes à l'application et exécutent 8 requêtes)
  • C'est donc une victoire d'Unicorn qui se tire mieux cette fois de la situation !

Conclusion

Je suis tenté de préférer Unicorn pour servir un site Ruby on Rails chargé. Cependant, il conviendra d'offrir suffisamment de workers à Unicorn pour qu'il puisse faire le travail correctement !

Quelques enseignements annexes :

L'impact de Pound sur les temps de réponse

Sans Pound, le temps de réponse moyen de Unicorn est de 117 ms. Avec Pound, toute autre chose étant égale par ailleurs, le temps de réponse moyen est de 161 ms. Il semble donc que le passage par Pound augmente le temps moyen de réponse d'environ 40 ms.

La multiplication des workers pour Unicorn : bonne ou mauvaise idée ?

La machine de test dispose de 4 coeurs. J'ai testé 4 workers et 16 workers avec Unicorn. Les résultats sont très similaires : respectivement 131 et 130 secondes pour effectuer le scénario de test complet. Les temps de réponse moyens sont là encore très proches : 113 ms vs 117 ms. Je ne pense pas que les différences constatées soient significatives. Les écart-types sur le temps de réponse diffèrent plus : 216 ms avec 4 workers et 265 ms pour 16 workers... Compte-tenu du nombre de coeurs sur la machine de test (4), l'utilisation de 16 workers ne semble pas permettre des gains substantiels. Un petit excès du nombre de workers par rapport au nombre de coeurs est peut-être à conseiller toutefois.

lundi 27 avril 2015

Exporter les droits d'accès ACL de tout un répertoire

Le petit script ci-dessous en Ruby parcourt l'arborescence d'un système de fichier et imprime dans une liste HTML le nom de chaque dossier et les droits d'accès ACL qui y sont associés.

@max_depth = 3

def parse(folder, depth)
  depth = depth + 1
  Dir.new(folder).sort.each do |f|
    puts "<ul>"
    if f != ".." and f != "." and File.directory?(folder+"/"+f)
      puts "<li"
      rights = `getfacl -p #{Shellwords.escape(folder+"/"+f)}`
      hashOfACLS = Hash.new
      rights.split("\n").each do |s|
      catch = /^group:(.*):([r-][w-][x-])/.match(s)
      hashOfACLS[catch[1]] = catch[2] unless catch.nil? or catch[1]==""
    end
    classes = ""
    details = ""
    hashOfACLS.each do |k, v|
      classes = classes + k + " " if v.include? "r"
      details = details + k + " => " + v + ";"
    end
    puts "class='#{classes}'>#{f} (#{details})"
     if depth < @max_depth
      parse(folder+"/"+f, depth)
    end
    puts "</li>"
  end
  puts "</ul>"
end
end

Ruby et les commandes shell

Ruby dispose d'un outil fort pratique pour "échapper" (traduction directe de "escape" en anglais) les caractères et mots clés réservés dans les commandes Shell : Shellwords.escape de la librairie Shellwords.

L'usage est tout simple :

  • on commence par charger la librairie
require 'shellwords'
  • et on peut ensuite échapper toute chaîne de caractère dans une commande lancée au Shell, par exemple :
resultat_de_la_commande = `getfacl -p #{Shellwords.escape(folder)}`

mardi 10 mars 2015

Rails 4.2, Thin et "Internal server error"

Un récent appel à une application développée avec Rails 4.2, et servie par Thin, m'a conduit à une erreur "500 Internal Server Error" dont je ne trouvais de trace dans aucun log de l'application, et aucun log pertinent pour le "load balancer" (pound) placé frontalement au-dessus de cette application.

Le plus étrange était de ne voir aucune trace dans les logs de Rails et notamment de ne voir aucune confirmation de la bonne réception de la requête par l'application Rails.

Après avoir exploré, ad nauseum, tous les potentiels blocages en amont de l'application Rails, j'ai fini par remplacer Thin par Webrick pour servir l'application : et là miracle, un message d'erreur plus explicite m'a été retourné en lieu et place de l'erreur 500 : "Unexpected error while processing request: Too many open files - Maximum file multiparts in content reached".

J'ai alors trouvé cette référence : http://stackoverflow.com/questions/27773368/rails-4-2-internal-server-error-with-maximum-file-multiparts-in-content-reached et celle-ci : https://github.com/rack/rack/pull/814

La première propose de contourner le problème en désactivant la limite sur le nombre d'éléments multipart en ajoutant cette ligne dans un fichier d'initialisation :

Rack::Utils.multipart_part_limit = 0

La seconde référence étudie la raison de ce comportement avec une mauvaise approche du comptage des balises dans le formulaire : "However, our form only had one file input. It had several hundred other form elements. Each was counted as a file.". Le commit en faute a été repéré et cela devrait être corrigé dans la prochaine version de Rails/Rake !

dimanche 17 août 2014

Nouvelle version d'atom (0.124) compilée pour Debian Jessie

Je viens de compiler la dernière version d'atom, l'éditeur de texte évolué créé par les équipes de GitHub. La version 0.124 est donc disponible sous la forme d'un paquet Debian : atom-0.124.0-amd64.deb.

Ce paquet devrait fonctionner sur Debian Jessie 64 bits.

Il a été généré en suivant les instructions de compuilation officielles et certains pré-requis doivent peut-être être remplis sur des machines qui n'ont jamais été confrontées à atom.

Bon téléchargement !

jeudi 27 mars 2014

Ordonner à Rails d'accepter un certificat auto-signé

Par défaut, l'implémentation OpenSSL dans Rails interdit (et c'est tout à fait normal !) les certificats dont l'identité ne peut être vérifiée. C'est ennuyeux si vous utilisez (par exemple pour des tests ou en développement) des certificats auto-signés car alors impossible de récupérer les données convoitées par exemple sur votre serveur HTTPS ou bien sur votre IMAPS.

Voilà l'erreur que OpenSSL doit retourner dans ce cas :

SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Pour contourner ce problème, on pourra inclure le code suivant dans son application :

require 'openssl'
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

Pour une application supportée par Rails, on pourra par exemple inclure ceci dans les "initializers", par ex. dans /config/initializers/bypass_ssl_identity_check.rb.

samedi 1 février 2014

Echec à l'installation du gem rmagick

Souhaitant utiliser le gem 'rmagick', je lance la commande classique :

18:22 pab@risotto /home/pab % gem install rmagick

mais suis récompensé par :

Building native extensions.  This could take a while...
ERROR:  Error installing rmagick:
	ERROR: Failed to build gem native extension.

    /home/pab/.rvm/rubies/ruby-2.1.0/bin/ruby extconf.rb
checking for Ruby version >= 1.8.5... yes
checking for gcc... yes
checking for Magick-config... no
Can't install RMagick 2.13.2. Can't find Magick-config

Après quelques recherches, je découvre qu'il faut préalablement installer libmagickwand-dev - ce que j'exécute sous Debian par la commande :

aptitude install libmagickwand-dev

samedi 18 janvier 2014

RVM et ruby regrettent l'absence de libyaml

Ce matin, après une installation fraîche de Ruby 1.9.3 et 2.1.0 avec RVM, j'avais la désagréble surprise de lire :

It seems your ruby installation is missing psych (for YAML output).
To eliminate this warning, please install libyaml and reinstall your ruby.
ERROR:  Loading command: install (LoadError)
	cannot load such file -- zlib
ERROR:  While executing gem ... (NoMethodError)
    undefined method `invoke_with_build_args' for nil:NilClass

Je dus exécuter les commandes suivantes : (i) forcer l'installation de libyaml par RVM

rvm pkg install libyaml

(ii) réinstaller les rubies installés :

rvm reinstall all --force

dimanche 14 juillet 2013

Utiliser RVM sans sudo

rvm est un utilitaire fort pratique pour utiliser aisément Ruby et les gems. Parfois, rvm demande à l'utilisateur le mot de passe pour utiliser sudo et installer certains paquets pré-requis (par exemple avant la compilation). C'est une fonctionnalité nommée 'autolibs'.

Si vous ne souhaitez pas utiliser sudo, il est possible de désactiver cette fonctionnalité par

rvm autolibs disable

Il faudra sans doute alors installer certains paquets pré-requis manuellement - la liste des paquets requis peut être connue par :

rvm requirements

Hackez bien !

mercredi 26 décembre 2012

ActiveRecord et le gem mysql2 rouspètent

Je rencontrai dernièrement un petit souci lors de la migration d'une application Rails depuis 3.0.1 vers 3.2.9 (la version stable la plus récente alors que j'écris ces lignes). En effet, au cours de la migration, j'étais invité à remplacer le gem mysql par mysql2 (donné comme plus rapide) et à ajouter le gem activerecord-mysql2-adapter.

Je m'exécutai et modifier le Gemfile en ce sens, mais au moment de lancer l'application - et de lancer des commandes sur la base de données via Rake - j'obtenais :

stack trace:
rake aborted!
undefined method accept' for nil:NilClass
/home/pab/.rvm/gems/ruby-1.9.3-p327@global/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/database_statements.rb:7:into_sql'

Et impossible d'aller plus loin...

Heureusement, les fils de discussion GitHub ont volé à mon secours : https://github.com/rails/rails/issues/7295

@nstuart yes, as @rafaelfranca says, you need to remove activerecord-mysql2-adapter from your Gemfile. The mysql2 adapter ships with Rails 3.1 and up, so you don't need this gem (in fact, that gem is causing your error).

et je comprenais qu'il fallait supprimer la ligne gem 'activerecord-mysql2-adapter' du Gemfile. Après une exécution de 'bundle install', tout rentra dans l'ordre.

dimanche 4 mars 2012

Configurer Rails mailer

Rails 3 est arrivé avec tout le nécessaire pour envoyer des courriels facilement.

La configuration s'effectue dans les fichiers d'environnement : pour un paramétrage fonctionnel dans l'environnement de développement on se concentrera sur le fichier /config/environments/development.rb.

Si l'envoi est effectué par un smtp externe :

  #Settings for send emails
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    :address => "smtp.myhost.tld",
    :port => 25,
    :domain => "emetteur.fr",
    :authentication => "plain",
    :user_name => "utilisateur",
    :password => "motdepasse"
  }

Si l'envoi est effectué par sendmail :

  config.action_mailer.delivery_method = :sendmail
  config.action_mailer.sendmail_settings = { 
    :location       => '/usr/sbin/sendmail', 
    :arguments      => '-i -t'
  }

La seconde méthode fonctionne avec le petit utilitaire 'sendmail' fourni avec nullmailer (pas un vrai 'sendmail', une émulation qui injecte le courriel dans la queue de nullmailer).

dimanche 6 novembre 2011

Ruby et mysql

Au cours d'un essai d'installation du gem 'mysql' pour Ruby, je rencontrais l'ennuyeux message suivant :

checking for mysql_query() in -lmysqlclient... no

Pour régler ce problème, il fallait installer libmysqlclient-dev !

checking for mysql_query() in -lmysqlclient... no

Même souci avec le gem 'pg' :

Can't find the 'libpq-fe.h header

Il fallait cette fois installer :

aptitude install libpq5 libpq-dev 

dimanche 12 juin 2011

Faire du ruby sur le web sans Rails : Sinatra

Sinatra peut être considéré comme une boîte à outil web pour Ruby très légère. Si Rails est bourré de fonctionnalités qui rendent la vie facile, il est parfois un peu démesuré d'utiliser Rails quand on veut juste exécuter un script Ruby et en afficher le résultat en ligne.

Une solution plus légère que Rails est Sinatra. [|http://www.sinatrarb.com/||http://www.sinatrarb.com/

Sinatra est un gem à ajouter à l'installation courante de Ruby :

gem install sinatra

On écrit alors le script en spécifiant les routes dans le code, par exemple :

#monApp.rb
require 'sinatra'

get '/hi' do
  "Hello World!"
end

get '/generate/:arg'
  generate(params[:arg])
end

def generate (arg)
  str=""
  5.times {
    str=str+","+arg
  }
  str
end

Ce code contient 2 routes : la première qui est appelée sur http://$host/hi et la seconde sur http://$host/generate/text.

On peut exécuter le script de manière locale en exécutant :

ruby -rubygems monApp.rb

(par défaut, le serveur de test est lancé sur http://localhost:4567)

Pour exécuter l'application via Passenger dans Apache, on ajoute dans le répertoire courant : - un dossier public (mkdir public) - un dossier tmp (mkdir tmp) - un fichier config.ru

qui contient par exemple le code suivant :

require 'rubygems'
require 'sinatra'
require '/path/to/myApp.rb'

root_dir = File.dirname(__FILE__)

set :environment, ENV['RACK_ENV'].to_sym
set :root,        root_dir
set :app_file,    File.join(root_dir, 'myApp.rb')
disable :run

run Sinatra::Application

On ajoute alors un VirtualHost convenable dans la configuration d'Apache et Passenger sert l'application Sinatra !