7
Dec/09
0

Mnesia, un dictionnaire transactionnel

Mnesia est une bête différente d’une base de données relationnelle. Il s’agit d’un dictionnaire clé/valeur transactionnel.

La conséquence de cette phrase est que toutes les opérations doivent être exécutées dans une transaction (même les lectures) et que les meilleures structures de données exploitables sont les enregistrements dénormalisés (pas de contrainte d’intégrité).

De même, il n’existe pas de système de requête type SQL, et la maîtrise des comprehension list est nécessaire afin de sélectionner des éléments.

Gestion des identités

Comme pour toute base de données, c’est une très mauvaise idée d’utiliser des données utilisateur en tant que clé d’identification de l’enregistrement.

Pour l’enregistrement suivant :

-record(user, {name, pass_hash, salt}).

la clé primaire correspond automatiquement au premier champ, donc le « nom » de l’utilisateur. Il est courant dans les différents sites de ne pas pouvoir changer son login une fois identifié (si utilisé comme clé primaire) ; cela permet de « simplifier » quelques requêtes quand à l’unicité du compte, mais je ne considère pas cela comme une bonne pratique.

Le mieux est de générer un identifiant indépendant et sans signification. Cet identifiant sera un UUID afin de rester au plus simple et au plus fiable au niveau implémentation.

Pour cela, on utilise un module de génération disponible sur le net : le notre sera celui de akreiling trouvé sur github (pour info, ce choix s’est fait après une recherche google seulement, sans trop d’analyse supplémentaire pour l’instant !!!! oui, je n’en suis pas très fier).

L’enregistrement devient :

-record(user, {uid, name, pass_hash, salt}).

et la création se fait en ajoutant un uuid :

#user{uid=uuid:srandom(), ...}

Gestion des erreurs

Une transaction mnesia ne semble pas générer d’erreurs. Pour faire planter le programme il est important de déclencher l’exception via une clause incorrecte.

Obligation de créer une API

Les fonctions mnesia sont vraiment très basiques. Il est obligatoire pour tout programme erlang de créer une API d’utilisation un peu plus flexible.

Toutes les applications seront donc plus ou moins obligées de redéfinir les méthodes de lecture, écriture, récupération globale d’éléments d’une table, de transaction, etc.

De plus, une des grosses difficultés de mnesia (c’est subjectif, je vous l’accorde) est de pouvoir écrire le code d’une façon lisible. On passe un temps trop important sur cette partie.

On va donc redéfinir ces opérations basiques comme suit :

write(Record) ->
  mnesia:transaction(fun() ->
    mnesia:write(Record)
  end).
 
tx(F) ->
  {atomic, Result} = mnesia:transaction(F),
  case Result of
    {error, _} -> throw(Result);
    _ -> Result
  end.
 
find(Q) ->
  F = fun() -> qlc:e(Q) end,
  case mnesia:transaction(F) of
    {atomic, Result} -> Result;
    _ -> []
  end.
 
find_all(Table) -> find(qlc:q([X || X <- mnesia:table(Table)])).

Si on ne fait pas ces méthodes, il sera beaucoup plus compliqué et lourd d’utiliser ce dictionnaire.

Ces fonctions ont été induites par l’utilisation de mnesia dans l’application. Un exemple d’utilisation de ces fonctions sera fourni ultérieurement.

Afin de pouvoir compiler le programme, il est important de pouvoir inclure les définitions des QLC. Selon le système, il peut être nécessaire d’introduire ce code en début de fichier :

-include("/usr/lib/erlang/lib/stdlib-1.16.1/include/qlc.hrl").

Le problème est l’utilisation d’un chemin en dur. Il est préférable de créer un lien symbolique au niveau du répertoire source :

cd src
ln -s /usr/lib/erlang/lib/stdlib-1.16.1/include

Puis d’utiliser ce chemin relatif dans le source :

-include("include/qlc.hrl").
Filed under: Erlang, mnesia
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
27
Oct/09
0

Amélioration des événements génériques an javascript

La problématique concerne la façon dont le MVC est addressé en javascript dans notre application.

Lorsque l’on veut répondre à un événement, on code de la façon suivante :

  1. création du lien HTML avec l’attribut data-event indiquant l’événement utilisé
  2. création de la fonction javascript permettant de répondre à cet événement
  3. création du code d’association entre l’attribut et la fonction

Ces cas sont représentés ainsi :

Création du lien :

<a href="xxx" data-event="eventStart">event</a>

Fonction javascript :

$.extend({
  eventStart: function() {
    console.log("eventStart fired");
  }
});

Code d’association :

$(document).bind("eventStart", $.eventStart);

La fonction utilisée afin de se substituer au click n’est pas représentée ici, elle peut être retrouvée dans un ancien article

Le problème est que l’on se retrouve rapidement à écrire une floppée de lignes d’association qui posent problèmes :

  • les lignes sont triviales et apportent beaucoup de bruit pour rien
  • introduit une duplication des noms d’événements non maintenable

Le mieux est de pouvoir inférer automatiquement ces lignes d’association à partir du javascript créé.

Résolution

Pour cela, on va stocker les événements génériques dans un autre espace de nom. On modifie donc nos événements en les stockant dans la clé events :

$.extend({
  events: {
    eventStart: function() {
      console.log("eventStart fired");
    },
 
    init: function() {
      $.each(this, function(key, value) {
        if (key != "init")
          $(document).bind(key, value);
      });
    }
  }
});

Et on rajoute par la même occasion, une fonction d’introspection qui permet d’associer toute fonction dans l’espace events à l’événement correspondant au nom de la fonction !

Il suffit alors de rajouter un comportement global de type :

$(function() {
  $.events.init();
}

Et tout lien qui possède l’attribut data-event sera associé automatiquement à une fonction correspondante dans l’espace events.

La restriction est que tout événement ne doit pas comporter de caractères spéciaux. Nos événéments précédent comme login:start doivent être renommés en loginStart.

Maintenant, le workflow est le suivant :

  1. indiquer l’événement dans le lien à travers l’attribut data-event
  2. écrire la fonction ayant le même nom dans l’espace $.events

Simplissime, j’adore.

Extensions

Certains événements sont définis dans des fichiers séparés afin d’aider à la maintenance de l’applicatif.

Dans ce cas, il n’est pas possible d’utiliser la même notation, et il devient nécessaire d’augmenter la définition de la table de hachage de la façon suivante :

$.extend($.events, {
  moreEvent: function() {
    ...
  }
});
Filed under: javascript
26
Oct/09
0

Debug du mode rewrite sous nginx

Pour voir ce qui est fait au niveau du module rewrite de nginx, ne pas oublier de modifier la configuration de la manière suivante :

décommenter la trace de type notice :

error_log  logs/error.log  notice;

déclencher le mode rewrite au niveau du serveur HTTP :

http {
  rewrite_log on;

Les étapes de réécriture apparaissent dans le fichier de trace /etc/nginx/logs/error.log.

Filed under: Config, nginx
26
Oct/09
0

Internationalisation des vues

Moteur de transformation

L’application est bien évidemment internationalisée et donc disponible en plusieurs langues.

Le principe est le suivant :

  • On ne travaille que sur un seul modèle de vue
  • Tous les textes sont traduits dans un fichier de propriétés
  • Les vues sont uniquement statiques, pas de traduction à la volée

Le premier point est d’utiliser un mécanisme permettant de traduire les clés des fichiers modèles en traductions. Il est possible de définir son propre mécanisme de traduction assez simplement… cependant, ici, il semble plus intéressant de se reposer sur l’API fournie par la dernière version de Rails, qui est disponible en tant que gem et utilisable de façon autonome.

22
Oct/09
0

Bibliothèque javascript Pure ou mécanisme interne ?

Le site fait une utilisation intensive d’échange de données JSON. Ces données doivent ensuite être réintégrées au niveau d’une structure de page HTML.

Par exemple, en prenant la structure suivante :

<div>
  <div class="title">Template title</div>
  <div class="body">Template body</div>
</div>

Un fragment JSON reçut de ce type pourra être facilement intégré :

[{"title", "my new title"}, {"body", "a comment"}]

chaque clé sera utilisée comme filtre afin de déterminer l’élément HTML dont le corps sera remplacé. La clé est transformée en sélecteur jQuery de classe (« .title ») et le code HTML de l’élément est alors remplacé par la valeur de la clé.

Jusqu’à présent, il m’était évident que ces transformations devraient être réalisées par la bibliothèque pure. C’est d’ailleurs ce qui a été fait jusqu’à présent, comme indiqué dans les premiers articles.

Cependant, il est peut être intéressant d’explorer également une voie de fonctionnement plus simple.

Pure fonctionne sur un mécanisme de templates ou modèle de code HTML : la structure HTML utilisée est dupliquée et le résultat affiché n’est qu’une copie de la source. Ceci permet de créer des fragments de code qui peuvent être réutilisés.

Le problème est que si on fait un « rendu » de la vue complète (le tag body) en espérant que seuls les éléments indiqués seront impactés, ce n’est pas tout à fait le cas. Tout le code HTML du body sera remplacé, et donc tous les événements javascript associés de façon non obtrusive aux tags seront effacés !

Pure ne peut pas travailler sur les éléments finaux.

Je ne sais pas encore si ce schéma est pénalisant pour l’application ou non, car la gestion des différentes « vues » ou « pages » applicatives n’est pas encore réalisé.

En attendant, il est facile de proposer une version dégradée de ce fonctionnement qui réalise un remplacement et non un écrasement :

$.fn.extend({
  autoReplace: function(data) {
    $.each(data, function(key, value) {
      $(this).find("." + key).html(value);
    });
  }
});

Le remplacement sera effectif, sans détruire les événements associés au code affiché. Les limitations sont que seules les directives simples liées à la classe d’un attribut HTML sont gérées.

A voir avec les développements suivants, si une version simpliste de ce système est suffisant ou si l’on doit utiliser un mécanisme complet et complexe (car pure gère plusieurs bibliothèques javascript, une précompilation des modèles, etc.).

16
Oct/09
0

Modification des violations de vérification des données utilisateur

La liste des violations de données utilisateurs est structurée comme suit :

[{"login", "required"}, {"login", "min"}, {"pwd", "required"}]

Le problème est que la syntaxe JSON n’est pas correcte, et une des erreurs sur le login ne sera pas visible au niveau du javascript (du fait qu’il y a plusieurs clés identiques).

On modifie donc les validateurs pour qu’ils ne renvoient plus que la règle et non le tuple, par exemple le code suivant :

validator(Field, Value, required) ->
  case string:len(Value)==0 of
    true -> {Field, <<"required">>};
    false -> ok
  end.

devient :

validator(Value, required) ->
  case string:len(Value)==0 of
    true -> <<"required">>;
    false -> ok
  end.

Le paramètre Field n’est plus nécessaire.

C’est la méthode validate/3 qui sera chargée de produire le tuple final. On commence d’abord par écrire un test unitaire permettant de vérifier le comportement attendu :

test() ->
  ...
  [] = validate("login", [{"login", "a"}], [required]),
  [{"login", [<<"required">>,<<"min">>]}] = validate("login", [{"login", ""}], [required, {min_length, 6}]),
  ok.

On modifie alors la fonction pour satisfaire les tests :

validate(Field, Data, Constraints) ->
  Value = proplists:get_value(Field, Data),
  Errors = lists:map(fun(Constraint) -> validator(Value, Constraint) end, Constraints),
  Final = lists:filter(fun(Error) -> case Error of ok -> false; _ -> true end end, Errors),
  case Final == [] of
    true -> [];
    false -> [{Field, Final}]
  end.

Le reste du code est inchangé.

Filed under: Erlang