Dec/090
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").
Nov/090
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.
Nov/090
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.
Nov/090
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; } ...
- On active le moteur de réécriture (déjà intégré)
- On utilise un expression régulière avec capture afin de savoir si l’
URL
contient un paramètre de choix de langue, - on récupère la langue (limitée à 2 caractères, donc
fra
sera tronqué enfr
) - 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.
Nov/090
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.
Oct/090
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 :
- création du lien HTML avec l’attribut
data-event
indiquant l’événement utilisé - création de la fonction javascript permettant de répondre à cet événement
- 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 :
- indiquer l’événement dans le lien à travers l’attribut
data-event
- é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() { ... } });
Oct/090
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
.
Oct/090
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.
Oct/090
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.).
Oct/090
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é.