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.

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
16
Oct/09
0

Validation de formulaires en Erlang

Problématique

L’envoi des données de formulaire se fait par Ajax également.

Il est nécessaire de pouvoir sérialiser le formulaire afin d’envoyer les données sur le réseau, ainsi que de brancher le mécanisme côté serveur pour récupérer et valider les données.

Côté client, la sérialisation d’un formulaire se fait tout simplement en utilisant les mécanismes jQuery ; rien de spécial ici, on commencera par sérialiser un formulaire particulier avant de passer à une généralisation.

Côté serveur, nous allons mettre en oeuvre la récupération des données et leur validation. Tout ce qui vient du client doit être considéré comme non sûr et potentiellement corrompu.

La validation serveur est donc un processus absolument primordial. La pré-validation cliente est accessoire et ne sera pas traitée pour le moment.

Cette validation cliente participe à l’amélioration de l’expérience utilisateur. Pour notre part, avec l’architecture particulière mise en place où seuls les éléments mis à jour transitent entre le client et le serveur, on espère que la validation serveur ne sera pas trop pénalisante et donc suffisante dans un premier temps.

28
Sep/09
0

Mise en oeuvre de l’authentification (2 - niveau serveur)

L’article précédent a défini la façon dont le code utilisateur sera stocké dans au niveau du poste client, à savoir dans un cookie dont l’altération ne sera pas possible.

Maintenant, il est nécessaire de pouvoir mettre en oeuvre cette formulation au sein de l’application. Cette dernière sera principalement privée.

25
Sep/09
0

Automatisation des microtests Erlang via Rake

Les fonctions de tests unitaires commencent à apparaitre dans plusieurs fichiers, et il devient nécessaire de pouvoir régulièrement rejouer l’ensemble de ces tests. Ceci ne pouvant se faire que de manière automatique pour être efficace.

Attention on rappelle qu’il ne s’agit ici que de vérifier simplement et rapidement des comportements écrits directement dans les fichiers sources. Il est évident que pour des opérations plus complexes, un réel outil de tests unitaires tel que eUnit devra être mis en place.

Pour cela, on va réutiliser ce que l’on a déjà mis en place précédemment concernant l’exécution de la machine virtuelle Erlang via Rake (voir posts précédents).

Le but est de parcourir l’ensemble des fichiers sources .erl et déterminer pour chacun d’eux si une fonction tes/0 a été définie, auquel cas on exécute la méthode du module en question.

L’analyse du fichier sera une bête expression régulière, et pour cela on patch la classe File de Ruby afin de conserver une certaine harmonie dans l’écriture :

class File
  def self.contains?(filename, regexp)
    text = File.read filename
    return text.match(regexp) != nil
  end
end

Pas très efficace comme programmation mais nous ne sommes concernés que par des fichiers sources qui sont, par définition, des fichiers textes de petite taille.

On crée alors une tâche Rake :

desc "Unit tests"
task :test => :compile do
  sources.each do |source|
    if File.contains? source, /^test\(\) ->$/
      mod = File.basename(source, ".erl")
      puts "Unit testing #{mod}"
      system "erl -pa #{EBIN_DIR} #{ROOT}/deps/*/ebin -noshell -noinput -s inets -s crypto -s #{mod} test -s init stop"
    end
  end
end

Cette tâche dépend de la compilation afin de pouvoir jouer les fonctions.

On a repris la commande système utilisée pour lancer l’application. Certains paramètres ont été ajoutés :

-noshell -noinput # Exécution en mode console non interactive
-s inets -s crypto # Démarrage de modules nécessaires, ici crypto est lié aux cookies
-s #{mod} test # Exécution de la fonction test() du module mod
-s init stop # Arrêt de la machine virtuelle
22
Sep/09
0

Cookies d’authentification

Comme quasiment toute application web, il est nécessaire d’identifier l’utilisateur afin de proposer des traitements et parties de site spécifiques pour cet utilisateur (zones privées, profilage, etc.).

La tendance actuelle, concernant notamment la possibilité de mise à l’échelle ultérieure, est de stocker les éléments de fonctionnement liés à l’utilisateur (données d’état à conserver entre plusieurs requêtes) à l’intérieur de cookie.

Ce mode de fonctionnement permet de s’approcher d’une architecture de type Shared Nothing (SNA), ce qui est une des architectures les plus efficaces. Cela permet également de ne pas introduire d’entrée de jeu des mécanismes (sessions) qui sont fortement structurants et limitatifs.

Le problème de cette solution est la transition des informations complètes (et pas seulement un identifiant de session) sur le réseau et leur utilisation au niveau du poste client. Le protocol SSL étant consommateur et ne pouvant pas être utilisé sur l’ensemble d’un site, cela suppose de bloquer toute manipulation des données par un client.

18
Sep/09
0

Calcul de distances et tests unitaires

Le système nécessite un calcul de distances approximatif entre 2 points. Pour cela, on récupère le calcul théorique sur internet (par exemple, ici).

La transformation en distance est une reprise tel que des algorithmes :

distance(Coord1, Coord2) ->
  {Lat1, Lon1} = Coord1,
  {Lat2, Lon2} = Coord2,
  AngularDist = math:acos(
                 math:sin(Lat1 * math:pi() / 180) * math:sin(Lat2 * math:pi() / 180) +
                 math:cos(Lat1 * math:pi() / 180) * math:cos(Lat2 * math:pi() / 180) *
                 math:cos((Lon2 - Lon1) * math:pi() / 180)),
  AngularDist * 6371. %% km

Les coordonnées sont passées pour l’instant sous la forme d’un tuple comprenand la latitude et la longitude.

31
Jul/09
0

Première utilisation de CouchDB

Maintenant que la base est disponible, le but va être de remplacer le code JSON présent dans le contrôleur de test, par des données réelles - c’est à dire alimentées par la base documentaire.

Création d’un document

La première étape sera de créer un élément en base de données. Pour cela, nous utilisons futon.

En cliquant sur la base de données dans l’interface d’administration, on accède à un menu permettant de créer un nouveau document. Un document peut posséder n’importe quel schéma, c’est à dire n’importe quel ensemble de {clés, valeurs}. Lorsque l’on crée un document, le seul champ obligatoire à saisir est l’identifiant - qui peut être n’importe quelle valeur.