dimanche 28 septembre 2008

Transactions

Pfiou, c'était du sport!

J'ai enfin implémenté le système de transactions dans le cœur du programme.

Voilà le problème: de nombreuses actions des utilisateurs doivent être persistées de manière transactionnelle dans la base. Par exemple, effectuer un achat revient à ajouter le nouvel élément dans la base, et à mettre à jour le solde. Si l'application s'arrête (crash, coupure de courant...) entre les deux, il faut que la base revienne à l'état d'avant la première opération.

Ces transactions sont parfois complexes: par exemple, dans le cas d'un contrat entre deux joueurs, il va falloir mettre à jour les deux soldes, plus les deux inventaires pour les contrats, plus peut-être une autre table pour les modalités du contrat.

J'ai donc implémenté ma solution en passant une liste d'actions à effectuer lors de chaque opération se passant au niveau de la représentation du joueur sur le serveur. L'objet ajoute l'opération à la liste. Lorsque l'opération est finie, et que tous les joueurs ont rempli la liste, je l'exécute.

Voilà un exemple de log lors de l'achat d'un bâtiment: on voit la transaction démarrer, le solde se mettre à jour, la liste des bâtiments du joueur se mettre à jour également, et la transaction est commitée!


Starting transaction
Executing insert into Balance(operation_id, account_id, change, balance) values (default, $1, $2, $3)
Affected 1 rows in 0 ms
Executing insert into CorpBuilding(corpbuilding_id, account_id, building_id, level) values ($1, $2, $3, $4)
Affected 1 rows in 0 ms
Transaction finished in 0 ms

mardi 23 septembre 2008

Encore des icônes avec Inkscape

Des formes géométriques simples, des gradiants, et voilà le travail!

dimanche 21 septembre 2008

Asio et les timers - Une classe qui fait tout le boulot

J'avais précédemment causé des timers d'asio ici, mais vu que je viens de finir une importante sous-partie de mon sous-projet en cours (on a les jalons que l'on peut!), je vais m'étendre sur une petite classe outil qui m'a beaucoup simplifié la tâche.

Le code de la classe en question est un simple en-tête, disponible ici. L'idée est de dériver de la classe outil ainsi fournie en lui passant un paramètre template permettant de définir l'objet de callback, puis d'implémenter la méthode de callback. Voilà un programme minimaliste permettant d'utiliser ladite classe:


/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details. */

// Timer.cpp

#include <set>
#include <asio/io_service.hpp>
#include <asio/placeholders.hpp>
#include <asio/deadline_timer.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>

#include "Timer.h"

class MyTimer : public Common::Timer<std::pair<int, std::string> >
{
public:
MyTimer(asio::io_service & io_service):
Common::Timer<std::pair<int, std::string> >(io_service)
{
add_async_event(boost::posix_time::milliseconds(500),
std::make_pair(500, "Every 500 ms"));
add_async_event(boost::posix_time::milliseconds(1300),
std::make_pair(1300, "Every 1300 ms"));
add_async_event(boost::posix_time::milliseconds(1400),
std::make_pair(1400, "Every 1400 ms"));
}

void onTimer(const std::pair<int, std::string> & data)
{
std::cout << data.second << std::endl;
add_async_event(boost::posix_time::milliseconds(data.first),
data);
}
};

int main()
{
asio::io_service io_service;
MyTimer myTimer(io_service);
io_service.run();
}


Certes, il y a beaucoup d'inclusions: elles ne sont pas avec le fichier d'en-tête parce que je me sers de la pré-compilation d'en-têtes, et que donc tous mes fichiers sources ont un #include "All.h" qui contient tout le monde. Mais venons-en au fait.

Je déclare donc ma classe MyTimer, qui dérive de ma classe utilitaire Timer à qui je passe, par exemple, une paire comprenant un entier et une chaîne. J'implémente onTimer prenant une référence sur ce même type, et basta!

En l'occurrence, j'enclenche trois évènements, que je répète quand ils arrivent à expiration dans ma classe.

Une petite compile plus tard...


g++ -o Timer Timer.cpp -lpthread


Et voilà le résultat:


Every 500 ms
Every 500 ms
Every 1300 ms
Every 1400 ms
Every 500 ms
Every 500 ms
Every 500 ms
Every 1300 ms
Every 1400 ms
Every 500 ms
Every 500 ms
Every 1300 ms


Ainsi, chaque classe peut gérer elle-même ses évènements asynchrones. Si par exemple le service est également utilisé pour des connexions réseau, l'on peut gérer ensemble à la fois les interactions venues de l'extérieur, et les minuteries internes.

vendredi 19 septembre 2008

Une nouvelle icône

Promis, je vais finir par arrêter de poster mes icônes une par une. Mais pour l'instant, voilà mon symbole "recherche", dessiné une fois de plus avec Inkscape. Je demeure ébahi par la facilité avec laquelle il est possible de faire des icônes simples avec cet outil:

* Un triangle et deux rectangles pour la bouteille, avec un dégradé vert et un bord épais noir arrondi
* Des bulles à partir du même modèle, disque au contour noir, avec dégradé central blanc -> transparent, légèrement décalé. La bulle est alors joyeusement dupliquée

mercredi 17 septembre 2008

Postgresql - Optimisation d'une requête

Schématiquement, j'ai une table des opérations, qui maintient pour chaque opération un identifiant unique (incrémental), et l'utilisateur qui a amorcé l'opération. Vu que chaque opération maintient l'état final du compte de l'utilisateur, je n'ai besoin de charger au démarrage que la dernière opération pour chaque utilisateur.

Je fais tourner le code suivant dans pgAdmin3.

Tout d'abord, je créé la table des opérations, et je la remplis avec des données aléatoires. J'ai donc 10 millions d'opérations, réparties sur 10 000 utilisateurs.


create table data(id int not null, utilisateur int not null);
insert into data select generate_series(1, 10000000), (random()*10000)::int;


La requête qui m'intéresse est simple:


select max(id), utilisateur from data group by utilisateur;


Arg, 6.3 secondes! Il y a forcément moyen de faire mieux. Un petit tour auprès du plan (F7) nous indique qu'effectivement, la requête balaye toute la table. Mais bien sûr, rajoutons un index! Si l'index est sur l'utilisateur, puis l'id, le plan devrait être trivial et bien plus efficace.


create index data_idx on data(utilisateur, id);
vacuum full analyze;


On relance la requête... Et aucune différence. Pourtant, on sent bien que l'index devrait aider! Mais Postgresql est historiquement un peu faible au niveau des performances des agrégats. Il va donc falloir forcer un peu le plan. Par exemple, en forçant la base de données à commencer à lire la liste des utilisateurs, puis en allant chercher l'identifiant le plus haut. Cela tombe bien, dans mon modèle, il y aura une liste séparée des utilisateurs. Essayons:


create table utilisateurs(utilisateur int primary key);
insert into utilisateurs select distinct utilisateur from data;


Et maintenant, la requête modifiée:


select utilisateur, (select max(id) from data d where d.utilisateur = u.utilisateur)
from utilisateurs u;


Yeah, 370 ms! Bien mieux, et parfaitement en phase avec mes besoins.



En regardant le plan, l'on se rend compte qu'il se passe exactement ce que nous voulions: pour chaque utilisateur, notre index est utilisé pour trouver le maximum. L'on notera que Postgresql a de fait transformé notre requête en ceci:


select
utilisateur,
(select id from data d where d.utilisateur = u.utilisateur order by id desc limit 1)
from utilisateurs u;


Dans les versions précédentes, le planificateur ne savait pas optimiser les max et min, et la meilleure manière d'être efficace était alors d'ordonner les données et de limiter la sortie. Dans 8.3 (ou peut-être 8.2?), le planificateur le fait de lui-même.

Voilà notre requête environ 20 fois plus rapide, cela valait le coup de s'y accrocher, non?

Petit tutorial Inkscape - Le symbole "Radiations"

Simplissime tutorial Inkscape pour dessiner le symbole du danger de radiations

Tout d'abord, commençons par un camembert bien fait!



Dessinez un cercle (utilisez la touche Control pour le rendre régulier), passez en mode éditer les poignées, et déplacez la poignée ronde en gardant le pointeur à l'extérieur du cercle. Tout en maintenant la touche Ctrl, ouvrez votre camembert de 4 graduations, correspondant à 60 degrés.



De retour en mode sélection, cliquez deux fois sur la forme pour voir les poignées de rotation. Déplacez la petite croix représentant le centre de rotation au bout du camembert. Dupliquez l'objet (Ctrl-D) et effectuez une rotation de 120 degrés en maintenant la touche Ctrl. Dupliquez à nouveau, et ré-effectuer une rotation. La forme devrait maintenant ressembler à ceci.



Groupez les trois camemberts (sélection, Ctrl-G), puis ajoutez un petit cercle noir au milieu (vous pourriez avoir à re-sélectionner l'icône "cercle entier"), puis un cercle jaune un peu plus grand, que vous baisserez juste en dessous du cercle noir (Pg Down), puis un grand cercle jaune enrobant toute la forme, que vous mettrez au niveau le plus bas (Répéter Pg Down). Ne vous occupez pas trop de centrer pour l'instant.



Sélectionnez tout, utilisez l'outil d'alignement, et alignez tout au centre dans l'axe vertical et l'axe horizontal.



Ajustez le grand cercle jaune vers le bas pour bien le centrer, et voilàte!

Voilà mon icône finie (avec le petit éclair pour les besoins du symbole).

jeudi 11 septembre 2008

Un peu de sel dans les passwords

Il a suffi d'un regard vers ma table de passwords pour me rendre compte du problème: tous mes passwords de test avaient le même hash. Normal, les passwords sont identiques...

Se contenter de hacher les mots de passes avec du md5 les rend particulièrement vulnérables à une attaque de type dictionnaire, où l'attaquant génère des hashs de mots connus et les compare à toutes les entrées dans la table. L'augmentation du nombre d'entrées augmente du même coup la probabilité de faire mouche.

La méthode standard pour renforcer le hachage est d'introduire un "salt", petit mot aléatoire au début du mot de passe. Je me suis donc attelé à la tâche, et voilà les deux fonctions pour Postgres qui génèrent et vérifient les mots de passe.


-- createPassword (login, password)
create function createPassword(varchar(255), varchar(255)) returns varchar(255) as
$$
select salt.s || md5(salt.s || $1 || $2) from
(select substr(md5(now()::text), 0, 5) as s) as salt
$$ language 'sql' volatile;

-- matchPassword (login, password, hash)
create function matchPassword(varchar(255), varchar(255), varchar(255)) returns boolean as
$$
select (salt.s || md5(salt.s || $1 || $2)) = $3 from
(select substr($3, 0, 5) as s) as salt;
$$ language 'sql' immutable;


Disclaimeur: Si vous faites quoi que ce soit de sérieux avec ces fonctions, prenez conseil auprès d'un pro de la cryptographie. Notamment, je suis sûr que générer le salt à partir d'un hash de l'horloge sera considéré comme une faiblesse

mardi 9 septembre 2008

Une planète

Maintenant, c'est Gimp que j'essaie de dompter. Un petit tutorial plus tard, voilà le travail:

samedi 6 septembre 2008

Une petite dernière

Une pièce avec Inkscape. C'est étonnant comme l'ajout d'un simple dégradé améliore énormément l'image! En l'occurence, je ne m'inquiète pas trop pour les détails, puisque la pièce est destinée à être vue en taille 32 par 32 pour des icônes.

Et une autre sphère!

Décidément, je commence à vachement accrocher sur Inkscape!

Programmer's art

Parce qu'il faut savoir varier les plaisirs, voilà un exemple d'orbe que je viens de dessiner avec Inskcape, basée sur ce tutorial, et cette traduction à laquelle l'auteur (béni soit-il!) a ajouté quelques explications supplémentaires. Je trouve le principe de superposer un dégradé semi-transparent au dessus du premier dégradé absolument génial. Voilà quelque chose que je n'aurais jamais trouvé tout seul.

mercredi 3 septembre 2008

Le Xml est comme la violence: si cela ne résoud pas votre problème, mettez en plus

Il me semblait bien avoir ici parlé de xsd, générateur de code C++ à partir d'un schéma xml, et qui a fait depuis peu son arrivée dans Debian, à ma plus grande joie.

En effet, je m'efforce depuis un certain temps à n'utiliser que des paquets directement disponibles sur les serveurs officiels de ma distribution. Auparavant, preux chevalier du ./configure make makeinstall, je remplissais ainsi mon /usr/local avec des trucs pas forcément faciles à retirer ensuite, provoquant d'innombrables soucis de compatibilité avec des logiciels plus récents qui s'installent via les mises à jour de la distribution. C'est maintenant fini, je n'installe que via aptitude. Et s'il y a un logiciel que je veux vraiment compiler moi-même, je le garde dans mon répertoire (./configure --prefix=~/local est mon ami), là où il est bien moins risqué de jouer du rm à tout va.

Mais je m'égare, et je reviens à xsd. Pour avoir le malheur d'utiliser tous les jours la Concurrence (non, pas de noms!), je puis vous dire que xsd est un plaisir à utiliser, et que j'apprécie tout particulièrement sa légèreté. On génère, on compile, la seule dépendance étant xerces. la Concurrence, donc, souvent requiert une lourde bibliothèque additionnelle, des DLLs, et génère des DLLs. Bref, l'horreur. Car, c'est bien connu, Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl DLL fhtagn, ce qui veux dire "Dans sa demeure de R'lyeh, le défunt Cthulhu rêve de DLLs et attend", ce qui prouve bien leur origine épouvantable.

Mais la raison première de ce post est de publier ces quelques petites lignes, qui, si elles ne servent à personne d'autre, seront au moins pour moi un bon pense-bête. C'est l'ajout d'une cible omake pour générer mon schéma.


.PHONY: schema
schema:
xsdcxx cxx-tree --root-element-all --generate-serialization --hxx-suffix .h
--cxx-suffix .cpp --namespace-map =Schema xsd/*.xsd


Attention, les deux lignes "xsdcxx" sont censées n'en être qu'une seule, mais sinon c'était trop long et c'était le bordel pour faire tenir tout ça sur le blog. Attention², xsdcxx est le nom de xsd sous Debian, puisqu'il existait déjà un utilitaire nommé xsd.

--root-element-all permet que chaque élément défini au plus haut de la hiérarchie puisse être un élément racine, c'est à dire ayant des fonctions pour sérialiser. Si vous n'avez qu'un seul document principal, vous pouvez vous en tamponner le coquillard, mais pour moi qui définit parfois plusieurs messages indépendants par fichier, c'est primordial.
--generate-serialization Plutôt utile pour faire quoi que ce soit avec le fichier sans avoir besoin de passer par les fonctions xerces.
--hxx-suffx et --cxx-suffix Moi, j'aime bien que mes fichiers soient appelés tous pareil. Mes en-têtes sont en .h, mes sources en cpp. Et pas autrement!
--namespace-map Les namespace XML, encore un concept tordu comme seul XML peut les corrompre. Cette commande permet de définir comment transformer un namespace XML en un namespace C++. Pour éviter de finir comme un héros Lovecraftien (mais le XML, c'est plus Dieux Extérieurs que Grands Anciens), je ne définis aucun namespace dans mon XML, mais j'indique à xsd que je veux avoir tout le code dans un namespace à moi.

À noter, la commande --output-dir qui permet d'éviter que tout le schéma ne se déverse dans le répertoire courant.