24
Nov/09
0

Simplification des appels à erlang depuis ruby

Dans les articles précédents, les appels au processus erlang via la ligne de commande étaient écrits ainsi :

system "erl -pa #{EBIN_DIR} #{ROOT}/deps/*/ebin -boot start_sasl -s reloader -s inets -s xxx -mnesia dir 'db'"

On améliore l’écriture de cette commande afin de la rendre plus lisible et maintenable. Pour cela, on crée une fonction wrapper comme ci dessous :

def erl(args, options)
  dirs = "-pa #{options[:dirs].join(' ')}" if options[:dirs]
  run = "-s init stop" if options[:init]
  noshell = "-noshell -noinput" if options[:noshell]
  func = "-s #{options[:func][0]} #{options[:func][1]}" if options[:func]
  mods = options[:mods].map { |e| "-s #{e}" }.join(' ') if options[:mods]
  boot = "-boot #{options[:boot]}" if options[:boot]
 
  puts "erl #{dirs} #{boot} #{noshell} #{mods} #{args} #{func} #{run}"
  system "erl #{dirs} #{boot} #{noshell} #{mods} #{args} #{func} #{run}"
end

La commande peut être ré-écrite ainsi :

erl "-mnesia dir 'db'",
    :boot => "start_sasl",
    :mods => ["reloader", "inets", "xxx"],
    :dirs => [EBIN_DIR, "#{ROOT}/deps/*/ebin"]

Ce qui, me semble, est plus compréhensible que précédemment. On peut remarquer que le cas de la base mnesia n’a pas encore été pris en compte.

Filed under: Erlang, Rake, build
24
Nov/09
0

Mise en place de Mnesia dans mochiweb

Introduction

Les données de l’applicatif proviennent de plusieurs sources, et sont stockées différemment selon les besoins.

Par exemple, couchDB ne sera pas utilisé pour stocker les informations d’authentification des utilisateurs. Pour cela, on s’appuiera sur la base de données mnesia fournie avec erlang.

Lancement

La première action à faire est de modifier le fichier Rakefile.rb afin de démarrer le module mnesia au démarrage de l’application :

task :start => [ :build, 'env:start' ] do
  system "erl -pa #{EBIN_DIR} #{ROOT}/deps/*/ebin -boot start_sasl -s reloader -s inets -s app -mnesia dir 'db'"
end

Les fichiers de données seront stockés dans le répertoire indiqué dans la ligne de commande. Le répertoire sera créé au moment de l’initialisation de la base.

Initialisation

Il est nécessaire ensuite de créer la base elle même. On pourrait ajouter le code nécessaire au niveau de la fonction erlang de démarrage applicatif. Cependant, on va créer une tâche et une fonction spécifique à cette initialisation. Au démarrage de l’application, on suppose que la base est créée… sinon le système plantera.

Pour cela, on crée une tâche d’initialisation dans le fichier Rakefile.rb :

namespace 'db' do
  task :create do
    system "erl -pa #{EBIN_DIR} -s xxx_db create -mnesia dir 'db' -s init stop"
  end
end

On démarre la fonction create du module xxx_db, et les données seront stockées dans le même répertoire que vu précédemment.

Ce module est un fichier créé dans le répertoire des sources src/xxx_db.erl, et défini ainsi :

-module(xxx_db).
-export([create/0]).
-include("records.hrl"). (a)
 
create() ->
  mnesia:delete_schema([node()]),
  mnesia:create_schema([node()]), (b)
  mnesia:start(),
 
  mnesia:create_table(user, [{disc_copies, [node()]}, {attributes, record_info(fields, user)}]), (c)
 
  mnesia:stop().

Une base mnesia requiert un espace d’adressage qui comporte un certain nombre de propriétés. C’est ce qui est défini dans la création du schéma (b), ici les propriétés par défaut sont positionnées.

On démarre le moteur mnesia, et on créé les tables nécessaires (c) en suivant les définitions. Ces définitions sont introduites par une directive non vue encore (a) qui permet d’inclure des fichiers de définition. Ce fichier contient la définition des tables et se trouve dans le répertoire source src/records.hrl. Il a cette forme :

-record(user, {name, password}).

Les records sont une facilité d’écriture fournie par erlang et représentent des tuples nommés. Ce fichier sera inclus dans l’applicatif dès que l’on devra manipuler une table mnesia.

Important : mnesia considère que le premier champ du record représente sa clé ! Cette dernière pourra être utilisée pour une interrogation directe.

Note : De même, on peut créer la fonction init/0 qui pré-rempli la base avec des données de référence.

Intégration mochiweb

Mnesia doit être démarré comme tout autre service lors du lancement de notre applicatif. On modifie donc le fichier de démarrage src/xxx_app.erl afin d’intégrer ce comportement :

start(_Type, _StartArgs) ->
  application:start(crypto),
  application:start(mnesia),
  application:start(ecouch),
  xxx_deps:ensure(),
  xxx_sup:start_link().
 
stop(_State) ->
  application:stop(ecouch),
  application:stop(mnesia),
  application:stop(crypto),
  ok.

A partir de là, la base mnesia peut être interrogée par l’applicatif. Ces interrogations se font directement par les fonctions de l’API mnesia ou bien par le système qlc basé sur des expressions de listes.

13
Nov/09
0

Redirection nginx des vues par locale

L’internationalisation demande à ce que l’on serve des fichiers différents selon la locale sélectionnée. Ces fichiers sont générés par une pré-génération via Rake.

Maintenant, il faut être capable de choisir le fichier à fournir à l’utilisateur. Nous utiliserons principalement la sélection de la locale par paramètre d’URL, ceci afin de respecter des principes REST d’auto-suffisance des URL et parce que les accès aux ressources se font principalement par Ajax et sont donc cachées à l’utilisateur et automatisable simplement.

les URL seront donc de la forme suivante :

  • http: /view.html -> page avec locale par défaut -> filesys: /www/view_en.html
  • http: /view.html?lang=fr -> page traduite en français -> filesys: /www/view_fr.html

Pour cela, il est nécessaire de traduire un paramètre de requête en un chemin dans le système de fichiers directement au niveau du serveur web : nginx nous fourni un module de réécriture comme sur tout autre serveur.

On le met en oeuvre de la manière suivante :

 
http {
  server {
1   rewrite_log on;
2  if ($args  ~* lang=(..)) {
3     set $lang $1;
4     rewrite ^(.+)\.html$ $1_$lang.html last;
    }
 
...
  1. On active le moteur de réécriture (déjà intégré)
  2. On utilise un expression régulière avec capture afin de savoir si l’URL contient un paramètre de choix de langue,
  3. on récupère la langue (limitée à 2 caractères, donc fra sera tronqué en fr)
  4. on réécrit le nom de la ressource pour inclure la locale

Remarque : la réécriture ne concerne pas les paramètres, donc la règle de réécriture est correcte et conserve les paramètres.

Filed under: Config, nginx
5
Nov/09
0

rush, un peu plus d’OOP dans les scripts

Problématique

Le lancement de l’application se fait par une tâche Rake, comme décrit précédemment dans le premier article concernant les builds.

task :start => :build do ...

La tâche de lancement est dépendante d’un build complet.

Cependant, on ne prend pas en compte l’environnement, et différents processus doivent être lancés avant de démarrer l’application. Il s’agit d’une dépendance sur des processus externes, et cela est généralement réalisé par des appels système.

Rush

Cependant, les commandes à créer sont un peu complexes et lourdes à maintenir. Pour cela, nous utiliserons donc une application Ruby qui s’inscrit dans la direction des shell basés sur de la programmation objet, comme le powershell de Microsoft.

Bienvenue à Rush. Rush a été créé pour les tâches d’administration liées au projet Heroku comme expliqué dans un article de l’auteur.

Vous pouvez également consulter une présentation de rush, qui expliquera bien mieux que moi les intérêts d’une telle technologie.

On l’installe comme tout autre gem :

gem install rush

A noter que je suis passé à gemcutter pour la gestion des modules (nouveau site officiel de gestion des gems).

ATTENTION ! Rake n’est pas vraiment compatible avec les systèmes de shell (et ceci m’a coûté beauuucoup de temps). En effet Rake redéfini les entrées et sorties standard, ce qui fait qu’il n’est plus possible de lire sur l’entrée standard, et donc qu’il est impossible de rediriger des commandes par pipe. C’est pourquoi les commandes shell seront écrites dans un fichier séparé.

Si vous insistez, vous vous retrouverez avec cette erreur peu compréhensible :

rake aborted!
Broken pipe

Start

On peut étendre la tâche de démarrage pour ajouter une dépendance à l’environnement :

task :start => [ :build, 'init:env' ] do 
end

et créer une tâche dans l’espace init :

require 'rush'
 
namespace 'init' do
  desc "Init servers"
  task :env do
    app = Rush[ROOT]
    app["*.dump"].each{ |f| f.destroy }
    app["*.swp"].each{ |f| f.destroy }
    system "#{ROOT}/process.sh"
  end
end

Ici, on efface des fichiers temporaires à chaque nouveau démarrage en utilisant la syntaxe rush (ROOT est une constante qui représente le chemin racine du projet).

Un point important, comme indiqué précédemment, c’est que la gestion des processus est délélguée à un fichier externe, et lancée dans un shell externe par la commande ruby directement. Ceci car rake et la gem session ne sont pas compatibles.

En tout cas, il n’est pas possible de démarrer une session bash dans une tâche rake, l’inverse étant sûrement possible.

Gestion des processus

La tâche d’initialisation a pour but de vérifier que les processus serveurs sont lancés, et si non de les lancer. Les serveurs sont actuellement Nginx et CouchDB.

On peut écrire le code ainsi (dans process.sh) :

#!/usr/bin/ruby -rubygems
 
require "rush"
 
if Rush::box.processes.filter(:cmdline => /nginx/).empty?
  puts "Starting nginx"
  Rush.box.bash "nginx", :user => "root"
end
 
if Rush::box.processes.filter(:cmdline => /couchdb/).empty?
  puts "Starting couchdb"
  Rush.box.bash "couchdb -b", :user => "root"
end

Et les processus ne seront démarrés que s’ils n’existent pas.

La commande indiquée dans le site est la suivante :

if Rush::box.processes.filter(:cmdline => /nginx/).alive?

Malheureusement, cela ne fonctionne pas du fait que les processus sont lancés en tant que root, et je n’ai pas trouvé comment contourner ce problème.

Filed under: Rake, build