Gestion des dates et heures dans les bases de données

Je vais vous parler d’une réflexion que j’ai en ce moment, causée par un développement sur un produit qui se veut à destination d’utilisateurs répartis autour du globe.

Jusqu’ici, je n’avais jamais vraiment été confronté à de véritables problèmes de gestion des dates dans mes applications. Je stockais les dates et heures en base de données dans des champs de type DATETIME (avec MySQL). Les dates enregistrées l’étaient en partant du principe qu’elles étaient valables en France, et donc au moment de lire les dates pour les afficher cela ne posait pas de soucis pour des utilisateurs situés en France là encore.

Notez bien que j’utilise MySQL, qui a la très mauvaise idée de stocker les dates sans la moindre information de timezone. Donc quand vous enregistrez la valeur ‘2011-04-12 11:04:12’, il l’enregistre telle quelle, sans plus d’information. À vous de vous débrouiller avec ça. Encore une fois, tant que vous êtes dans le même pays pour la lecture et l’écriture, cela ne pose aucun problème. Ça devient plus délicat quand ce n’est pas le cas.
Je vais m’expliquer en détail, mais sachez qu’avec d’autres moteurs de base de données, comme PostgreSQL par exemple (je ne connais pas les autres ; j’ai travaillé un peu avec Oracle il y a 10 ans, mais je n’ai pas de souvenir à ce niveau), il y a des champs permettant de gérer les timezones, ce qui évite bien des soucis la plupart du temps.

Le cas d’utilisation problématique apparaît lorsque vous devez lire une date et l’afficher pour un utilisateur situé dans un fuseau horaire différent de celui qui a été à l’origine de l’enregistrement.
Ce qui est normalement prévu, c’est de convertir la date depuis sa timezone de départ vers la timezone d’arrivée. Si on part du principe que la timezone utilisée par le serveur n’a pas été modifiée, on peut se baser dessus pour faire la conversion. Pour cela, on peut utiliser la fonction CONVERT_TZ(). Par exemple, pour lire une date enregistrée en France et l’afficher pour un utilisateur québécois, on peut écrire ceci :

SELECT CONVERT_TZ(date, @@session.time_zone, '-5:00') FROM MaTable;

Le résultat est que la date stockée dans le champ date est convertie, depuis la timezone définie par le système (ou éventuellement dans la configuration de MySQL), vers une date dont la timezone a 5 heures de retard sur le fuseau zéro.
Donc si la date en base est ‘2011-04-12 11:04:12’, que le système a une timezone par défaut configurée pour être celle de la France, qu’on est actuellement en hiver (donc la timezone serveur est ‘+01:00’), le résultat sera ‘2011-04-12 05:04:12’.

C’est parfait !

Oui mais non. En Europe, le passage à l’heure d’été se fait le dernier dimanche de mars, alors qu’en Amérique du Nord il se fait le deuxième dimanche de mars ; les passages à l’heure d’hiver se faisant respectivement le premier dimanche de novembre et le dernier dimanche d’octobre. Il y a donc 3 semaines dans l’année pendant lesquelles il n’y a plus que 5 heures de décalage horaire entre la France et le Québec, au lieu des 6 heures habituelles.

Imaginons que je convertisse la date ‘2010-03-21 15:27:10’, par la même manière que précédemment, que se passe-t-il ?

CONVERT_TZ('2010-03-21 15:27:10', @@session.time_zone, '-05:00')

Il faut comprendre que le premier paramètre est indépendant des deux suivants. Le deuxième paramètre prend la valeur de la timezone locale, définie par le système ; ce qui me donne ‘+01:00’ (décalage en France en hiver) ou ‘+02:00’ (décalage en France en été). Mais pour le troisième paramètre, j’ai donné une valeur fixe, qui va s’ajouter au paramètre précédent.
Donc, suivant que je fasse cette conversion en été ou en hiver (et donc suivant la valeur du second paramètre), le décalage horaire qui va être calculé sera de 6 ou de 7 heures.

Ce n’est pas bon du tout ! Il n’y a jamais 7 heures de décalage horaire entre la France et le Québec !

Il faudrait donc que je sache moduler le troisième paramètre, pour lui donner une valeur différente en fonction de la date en cours (‘-05:00’ en hiver et ‘-04:00’ en été). Ainsi, le décalage sera toujours bien de 6 heures…
Sauf que la date en question (21 mars) tombe pile sur l’un des créneaux pendant lesquels il n’y a plus que 5 heures de décalage horaire entre la France et le Québec ! Comment faire pour interpréter cela correctement, et obtenir la bonne valeur (‘2010-03-21 10:27:10’, pour info) ?

Après avoir pas mal cherché, il semblerait qu’il ne faille pas faire confiance à MySQL. Du tout. Il ne sait pas gérer les timezones et encore moins les passages heure d’été/heure d’hiver ; donc quand il enregistre une date, il la stocke telle quelle et il vous laissera vous débrouiller quand vous voudrez l’afficher.
Pour ne pas faire confiance à MySQL, le plus simple est de stocker toutes les dates telles qu’elles sont sur le temps UTC, donc sans décalage par rapport au temps universel. Au moment de la lecture, les dates doivent alors être récupérées de la même manière (temps UTC), puis converties au moment de l’affichage, en fonction des préférences locales de l’utilisateur.

Pour être plus clair, il suffit de faire la conversion au moment de l’écriture :

INSERT INTO MaTable SET date = CONVERT_TZ('2017-05-06 05:49:00', @@session.time_zone, '+00:00');

Au moment de la lecture, plus besoin de faire de conversion quand on lit les données en base. Par contre, il faudra convertir au moment de l’affichage, comme je l’ai dit plus haut.

Mais cette requête utilise la timezone locale du serveur, pour calculer le décalage horaire (ici ‘+01:00’ ou ‘+02:00’ si le serveur est en France, suivant qu’on soit en été ou en hiver au moment où la requête est exécutée). Si c’est un utilisateur qui fournit une date et heure, il va falloir récupérer sa timezone locale (‘-05:00’ ou ‘-04:00’ s’il est au Québec, suivant la période de l’année).

Par contre, si les seules dates que vous enregistrez sont les date et heure courantes, en utilisant la fonction NOW(), il peut être plus simple de configurer le serveur pour qu’il reste sur l’UTC.
Cela peut être fait en modifiant le fichier de configuration (‘/etc/mysql/my.cnf) :

default_time_zone = '+00:00'

Ou en modifiant le paramètre de la connexion en cours :

SET @@session.time_zone = '+00:00';

Quelques liens sur le sujet :

Normes utiles

Lorsqu’on développe ou simplement que l’on conçoit un système ou une application, il y a parfois des choses pour lesquelles il convient de faire des choix de manière éclairée. Ce qui est bien, c’est que des gens intelligents prennent du temps à normaliser tout un tas de concepts, et qu’il devient assez simple de bénéficier de cette intelligence.
Voici les quelques normes qui m’ont semblé importantes. Continuer la lecture de « Normes utiles »

Installation serveur HTTP(S) rapide

Voici un article qui va me servir à rassembler les différentes documentations dont je me sert à chaque fois que j’installe un serveur Web. Là, j’ai pris un serveur virtuel chez OVH (à 3 € HT par mois, pour 2 GO de RAM et 10 GO d’espace disque), sur lequel j’ai installé une distribution Ubuntu 16.04.

Une fois l’installation terminée, on reçoit un email avec le mot de passe root. Commençons donc par nous logguer sur la machine.

Installations de base

Installons quelques outils utiles :

# apt-get install tree htop iftop make screen subversion git

Créons un nouvel utilisateur, et ajoutons-le au groupe sudo (pour qu’il puisse exécuter des commandes en tant que root) :

# adduser toto
# usermod -aG sudo toto

Éditons la configuration SSH pour empêcher la connexion avec l’utilisateur root. Pour cela, il faut ouvrir le fichier /etc/ssh/sshd_config et éditer la ligne suivante :

PermitRootLogin no

Continuer la lecture de « Installation serveur HTTP(S) rapide »

Fonctionnement interne des langages de programmation

Juste pour le fun, voici quelques liens vers des sites qui détaillent le fonctionnement interne de plusieurs langages de programmation. C’est très intéressant à étudier.

Si vous connaissez d’autres sources d’information de ce type, n’hésitez pas à les ajouter dans les commentaires.

PHP
PHP Internals Book

Perl
Perl 5 Internals
Parrot

Lua
The Implementation of Lua 5.0 (PDF)
The Virtual Machine of Lua 5.0 (PDF)
A No-Frills Introduction to Lua 5.1 VM Instructions (PDF)

Java
The Structure of the Java Virtual Machine
JVM Internals blog

Python
Design of CPython’s Compiler
Python internals: Working with Python ASTs
Python’s Innards

Ruby
Ruby Under a Microscope
Ruby Internals (70 slides)

Dart
The Essence of Google Dart: Building Applications, Snapshots, Isolates

Javascript
The V8 JavaScript Engine
V8 Internals: Building a High Performance JavaScript Engine

.NET / CLR
Internals to .NET
Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
.NET Type Internals – From a Microsoft CLR Perspective

Forth
A sometimes minimal FORTH compiler and tutorial for Linux (part 2)

Autres
The Potion Language

Edit : J’ai ajouté des liens Lua et Forth qui m’ont été transmis par @pchapuis. Encore merci !  🙂

Appel à avis : Handlers d’événement inline en Javascript

À la suite de mon dernier article, au sujet des technos navigateur, je prépare un autre billet consacré aux langage alternatifs qui génèrent du Javascript. Et au détour de tout ça, je suis tombé sur une page de la documentation Dart qui m’a un peu interloqué.

Cette page, consacrée à l’intégration de Dart dans du HTML, parle entre autre des handlers d’événement inline (désolé pour le franglais, je n’ai pas trop de synonyme correct qui me vienne spontanément à l’esprit). Vous savez, c’est le fait de mettre un “onclick” ou un “onchange” sur un élément HTML pour appeler une fonction Javascript lorsque l’événement en question est exécuté.

With JavaScript, programmers can embed inline event listener code directly onto HTML nodes. However, this is typically discouraged in modern JavaScript applications. HTML pages generally render more quickly if listeners are added programmatically afterwards. Modern security guidelines also discourage inline code. As such, we propose to not allow inline Dart listeners.

J’ai été un peu étonné de lire ça. En cherchant 30 secondes sur le web, je suis tombé sur une question/réponse du site StackOverflow :

Most experienced developers shun this method, but it does get the job done; it is simple and direct.

Je fais donc appel à l’aide des lecteurs de ce blog.
Je code pas mal de Javascript ; pas autant que de PHP, mais ce doit être au final mon deuxième langage en quantité de code écrit (eh oui, même devant le C). J’écris du code que j’estime propre, en créant des objets et en les rangeant dans des namespaces, utilisant des callbacks là où ça se justifie, tout en se gardant du « callback hell ».

Et personnellement, j’ai toujours trouvé qu’il était plus facile de maintenir du code avec les appels d’événements indiqués dans le HTML, plutôt que de les positionner par Javascript. Quand on cherche ce qu’il se passe quand on clique sur tel élément, ou quand on tape du texte dans un champ texte, il est très facile de regarder le code HTML, voir quel méthode de quel objet est appelée, et enfin aller voir directement dans cette méthode.

Donnez-moi votre avis s’il-vous-plait.

Préférez-vous ce type de code :

<script>
var site = new function() {
    this.init = function() {
        // quelques initialisations
    };
    this.checkContent = function(text) {
        // un peu de code applicatif
    };
};
</script>
<input type="text" name="content"
 onblur="site.checkContent(this.value)" />

Ou plutôt celui-ci :

<script>
var site = new function() {
    this.init = function() {
        // initialisation du handler d'événement
        $("#edit").on("blur", function() {
            site.checkContent();
        });
    };
    this.checkContent = function() {
        var text = $("#edit").val();
        // un peu de code applicatif
    };
};
</script>
<input id="edit" type="text" name="content" />

Et pourquoi ?

Dart, NaCl, Pepper, Emscripten, PNaCl : Du nouveau du côté de la programmation sur navigateur

Comme tous ceux qui font du développement web, je code pas mal en Javascript. J’en ai déjà parlé sur ce blog, c’est un langage dont j’apprécie la souplesse mais dont les limitations m’empêchent d’imaginer faire du vrai génie logiciel dessus. J’ai beau faire des développements JS “propres”, avec l’utilisation d’objets rangés dans des namespaces hiérarchiques, je ne cesse de pester contre l’âpreté de son modèle objet.

Toutefois, pas mal de choses bougent en ce moment du côté du développement sur les navigateurs. Je n’ai pas encore vraiment mis le nez dans tous ça − à part lire de la documentation − mais ça reste intéressant d’en parler un peu.

Dart

Pour commencer, le langage Dart − créé par Google pour remplacer un jour le Javascript − vient de voir son SDK publié en version 1.0. C’est un langage qui fait voler en éclat les problèmes du Javascript ; son modèle objet est complet, il gère nativement les packages, et il peut être aussi bien fortement que faiblement typé.

Pour le moment, seule une version spécialement modifiée du navigateur Chromium (Dartium) est capable d’interpréter directement du code Dart. Mais il est possible de « compiler » du code Dart en code Javascript, permettant son exécution par n’importe quel navigateur. Là où ça devient intéressant, c’est que cette phase permet d’optimiser le code Javascript généré au point de le rendre en 42% et 130% plus rapide que le même code écrit nativement en Javascript.
C’est d’autant plus intéressant que les interpréteurs Javascript ont connu une énorme augmentation de leurs performances ces dernières années.

Bon, par contre je ne suis pas persuadé que passer par une phase de compilation pour du code client soit très pratique d’utilisation. Mais il reste toujours la possibilité de développer sur Dartium, puis de tester le code JS généré sur les autres navigateurs.

En tout cas, si le Go (l’autre langage de Google) a réussi à obtenir un peu de “traction”, j’ai le feeling que le Dart a le potentiel pour se faire une place de choix dans l’univers des langages de programmation modernes.

Programmation C/C++

Bon, quand il s’agit d’obtenir les meilleures performances possible, on finit quoi qu’il en soit par programmer en C ou en C++. Les ingénieurs de Google en étaient arrivés à cette conclusion et ont développé NaCl (pour “Native Client”) qui est une techno permettant d’exécuter dans un navigateur du code compilé pour processeur x86.
Enfin, quand on dit « dans un navigateur »… ça marche tant que le navigateur en question est Chrome, hein.

En plus, avec l’API Pepper, le code C/C++ n’est plus cantonné à une simple fenêtre, mais il peut communiquer avec le navigateur, ça peut amener à des choses sympatiques.

Mais fondamentalement, j’ai toujours trouvé cette idée bizarre. Oui, c’est génial d’avoir Quake qui tourne dans un navigateur web. Mais le principe fondamental du web, c’est de reposer sur des standards multi-plateformes. Embarquer du code compilé nativement pour un type de processeur au milieu de tout ça, c’est moche.

Ça tombe bien, car deux technologies différentes permettent de contourner ce problème. Les deux reposent sur une particularité du compilateur LLVM. Pour schématiser grossièrement, un front-end prend en charge la compréhension d’un langage de programmation, génère un bytecode spécifique, qui est ensuite utilisé par le back-end pour produire un exécutable natif. Et donc, le bytecode intermédiaire n’est pas dépendant du processeur sur lequel il va être exécuté.

La première techno basée sur ce bytecode s’appelle Emscripten. Elle prend du bytecode LLVM, et génère du code Javascript qui peut être exécuté directement par le navigateur. Le résultat est assez bluffant, dans la mesure où le moteur Unreal 3 a été porté en seulement 4 jours, et que la démo Epic Citadel qui est basée dessus est vraiment impressionnante.
En plus, avec la bibliothèque pepper.js, il est possible d’atteindre le même résultat qu’avec l’API Pepper (intégrée à NaCl, citée plus haut).

La seconde techno, issue de Google (décidément !), s’appelle PNaCl (pour Portable Native Client). Grosso modo, vous prenez NaCl, vous lui ajoutez Emscripten, vous mélangez bien, et voilà le résultat. En fait, le navigateur intègre un interpréteur de bytecode LLVM. On obtient ainsi le meilleur de chaque monde : la vitesse d’exécution qu’on est en droit d’attendre d’un code C/C++, avec la portabilité qu’on est en droit d’exiger sur le web.

Conclusion

Comme je le disais, ça bouge du côté du développement sur navigateur. Et c’est assez excitant. Je vais avoir du mal à trouver le temps d’expérimenter ces technos, mais j’en ai bien envie.

On peut remarquer que le vénérable Javascript, grâce à son support quasi-universel, reste le garant de la compatibilité de ces technologies (Dart, Emscripten) avec tous les navigateurs existants. Il devient le bytecode générique du web, un peu comme le C est utilisé par certains langages exotiques comme un bytecode intermédiaire (j’en parlais dans un article sur la création d’un interpréteur).
Pas sûr qu’il soit très simple ni rapide de faire une triple compilation (C/C++ vers bytecode LLVM, puis vers Javascript, pour enfin être interprété sur le navigateur, éventuellement avec de la compilation JIT). Mais si on peut accélérer le codage en développant sur un navigateur spécifique, pour ensuite être compatible avec tous les autres, ça peut en valoir la chandelle.

Plates-formes de développement privilégiées

Je réfléchissais dernièrement à une chose un peu particulière : le fait que plusieurs plate-formes informatiques ont eu des environnements de développement privilégiés, qui en sont devenus plus ou moins indissociables.

Psion – OPL

C’est en fait le couple auquel je pensais initialement, et qui m’a amené à écrire cet article. Le langage OPL (Open Programming Language) est un dérivé du Basic que la société anglaise Psion a intégré à ses ordinateurs de poche. Au milieu des années 80, ces machines n’étaient pas très sexy, et ressemblaient plutôt à des calculatrices améliorées, mais elle n’en étaient pas moins de vrais ordinateurs programmables.

Psion Organiser

Mais c’est avec les Psion série 3 que les choses prirent une dimension différente. Cette machine a été lancée en 1991 puis a été améliorée durant toute la décennie, avec des améliorations concernant la taille de l’écran, la puissance du processeur ou la mémoire embarquée (voir des copies d’écran du système).

J’ai personnellement possédé un Psion 3a pendant la seconde moitié des années 90. J’avais été marqué par la sortie du premier modèle quelques années auparavant. C’était le premier véritable ordinateur de poche.

Psion Series 3a

Plusieurs applications étaient embarquées : base de données, traitement de texte, tableau, agenda, … Mais surtout, ce qui a fait le succès de cet ordinateur à mes yeux, était la facilité avec laquelle n’importe qui pouvait créer de nouveaux programmes et les diffuser. Et cela était possible grâce à l’intégration du langage OPL et d’un environnement de développement sommaire mais fonctionnel dans le système.

Psion avait même eu l’intelligence de proposer des émulateurs de ses machines, qui tournaient sous MS-DOS. Ils permettaient de développer en utilisant un clavier plus pratique que celui des machines de poche. Ces émulateurs restent un bon moyen de faire revivre ces systèmes, grâce à DOSBox (voir ce lien).
Néanmoins, une énorme quantité de programmes ont été créés directement sur des Psion Série 3, et distribués par les différents moyens de l’époque (disquettes ou CD dans les magazines, puis internet).

Pourquoi l’OPL était-il si intéressant ? Parce qu’il était simple à apprendre − c’était un dérivé du Basic, en plus puissant pour l’époque − et qu’il proposait des routines graphiques faciles à mettre en œuvre, permettant de créer des interfaces graphiques sans trop de difficultés.

Pour la petite histoire, Psion a proposé par la suite un ordinateur plus performant, le Série 5, là encore avec l’OPL intégré. Puis le système du Psion Série 5 a servi de base au système Symbian, qui a longtemps été au cœur des téléphones Nokia et Ericsson.

Macintosh – HyperCard

L’exemple le plus marquant est sûrement le couple formé par HyperCard et le système d’exploitation des Macintosh à partir de 1987 et durant les années 90.

L’ordinateur qui innovait le plus à l’époque (en dehors de l’Amiga, bien sûr − petit troll gratuit), notamment par la simplicité d’utilisation de son interface graphique, aurait pu rester une machine difficile à programmer, que seule une élite aurait pu enrichir de leur logiciels. Mais le génie de Bill Atkinson a créé un outil qui mélange à la fois le logiciel de dessin, la base de données, le langage de programmation et l’hypertexte.

Pour comprendre comment fonctionnait HyperCard, vous pouvez regarder la vidéo proposée sur le site hypercard.org :

Je ne sais pas s’il existait déjà des outils de programmation WYSIWYG, mais HyperCard a clairement démocratisé cela.

Windows – Visual Basic

Autre cas emblématique, le Visual Basic qui est devenu l’un des environnements de développement les plus utilisés au monde, dont la première version a été lancée sous Windows 3 au début des années 90 (1991 pour être exact).

Lancé 4 ans après HyperCard, il en reprenait les principes de programmation événementielle sur la base d’une interface graphique créée à la souris, en y ajoutant des concepts provenant du couple Project Builder / Interface Builder présenté en 1988 sous NeXTSTEP.

Visual Basic

Le succès a commencé avec la version 3, qui s’appuyait elle-même sur le succès de Windows 3.1.

Pour avoir programmé en VB au milieu des années 90, je peux affirmer qu’un tel langage fait prendre plein de mauvaises habitudes quand on ne sait pas encore vraiment développer. Par contre, il permet de créer facilement et rapidement des applications relativement complexes, et son succès n’est absolument pas volé.

Mac OS X – XCode

Héritier spirituel du Project Builder déjà cité, XCode est l’environnement de développement offert gratuitement par Apple aux possesseurs de Mac. L’un de ses grands intérêts est de servir à la fois pour développer des applications pour Mac OS X, mais aussi pour iOS.

© Apple
© Apple

Sa gratuité et sa qualité générale en ont fait l’outil incontournable pour tous les développeurs Mac et iOS.

Et alors ?

Tout ça, c’est bien beau, mais à part l’aspect historique des choses, ça sert à quoi ?

En fait, j’ai deux réflexions qui me viennent à l’esprit.

La première, c’est que toute plate-forme informatique devrait fournir des outils de création gratuits et facile à prendre en main (on peut argumenter que le Visual Basic n’est pas gratuit, mais on connait aussi le taux de piratage des logiciels sous Windows, hein). Je ne parle pas d’outils ultra-pointus ; même quelque chose de limité permet toujours à des gens d’exprimer leur créativité. Je pense évidemment à des outils de développement ; mais c’est aussi valable pour de la création graphique (dessin bitmap, vectoriel, 3D), musicale, …
Il y a toutefois une légère différence : offrez un logiciel de dessin, et vous aurez des dessins ; offrez un outil de programmation et vous aurez des logiciels de dessins (et des dessins), des logiciels de musique (et des chansons), des éditeurs web (et des sites web), …

La seconde réflexion que je me suis faite, c’est qu’il faut toujours permettre aux gens de créer directement sur la plate-forme à laquelle est destinée les programmes qui sont développés. D’un côté l’exemple du langage OPL sur les Psion montre que cela a permis à tous les utilisateurs de ces machines de créer des programmes, sans attendre que des logiciels commerciaux remplissent tous les besoins.
De l’autre côté, je pense à GeoWorks Ensemble, une surcouche graphique de MS-DOS concurrente de Windows 3.x (apparue en 1990, soit 1 an avant Windows 3.0). Cette plate-forme était d’une qualité incroyable (multitâche, polices vectorielles, rapidité incroyable, …), mais nécessitait une station de travail Sun Sparc hors de prix pour développer des logiciels dessus. Échec commercial.

Je pense réellement que les terminaux de consommation passifs sont une chose qui ne devrait pas exister. Évidemment, la plupart des utilisateurs veulent pouvoir lire, regarder, écouter des média diffusés sur leurs appareils ; mais cela ne doit pas empêcher les autres utilisateurs de créer facilement et rapidement de nouveaux produits et services pour ces appareils.

Le futur ?

Ce qui m’embête un peu, c’est que j’ai donné des exemples plutôt anciens (à part XCode). L’informatique a évolué ces dernières années. Netbooks, ultraportables, tablettes et téléphone portables nous ont fait entrer dans une ère où l’ultra-mobilité règne.

Je trouve dommage que les éditeurs de systèmes pour tablettes imposent tous des SDK officiels qui s’utilisent uniquement sur ordinateur. Même si le SDK Android fourni par Google a l’avantage d’être multi-plateformes, il ne permet pas de coder directement sur une tablette Android.

Mais est-ce qu’un appareil mobile ne peut pas servir au développement ? L’exemple du Psion montre que si. D’ailleurs, il existe des solutions comme Codea (iOS) ou Droid Develop (Android) qui l’autorisent, et quand on regarde la vidéo de présentation de Codea, on se dit qu’il y a moyen de faire de très belles choses directement sur une tablette :

Pourtant, je reproche à ces solutions d’être encore trop proche du code. Il faut apporter des outils permettant de créer. Facilement. Directement sur l’appareil.

Est-ce que vous avez en tête d’autres exemples de plate-forme liée à une solution de développement privilégiée ?

FineDB : Gestion des timeouts

Si vous avez lu mon dernier article, consacré à FineDB, vous savez que l’une des évolutions que j’avais en tête était d’ajouter un thread dont le rôle aurait été de surveiller les connexions qui duraient depuis trop longtemps. Une manière de gérer les inactivités et de couper les connexions inutiles.

J’ai fait une implémentation complète de ce système. Cela imposait quelques modifications au code existant :

  • Gérer une option supplémentaire, pour définir la durée de timeout (avec une valeur par défaut à 90 secondes).
  • Ajouter un timestamp dans chaque thread, contenant la date de dernière activité − lecture ou écriture − du thread.
  • Créer un thread supplémentaire, qui parcours la liste des threads à intervale régulier. Ceux dont l’inactivité dure depuis un temps supérieur au timeout voient leur connexion coupée.

J’ai rencontré un premier problème. Lorsqu’une connexion était détectée comme trop longue, la socket équivalente était fermée avec la fonction close(). Malheureusement, cela n’est pas suffisant ; le thread de communication restait bloqué à son appel à la fonction read(). Le fait de fermer la socket avec close() envoie l’information au client situé à l’autre bout de la connexion, mais cela ne change rien pour le read() local, qui attend toujours de recevoir des données.

L’astuce est de faire un appel à shutdown() au lieu de close(), en lui donnant le flag SHUT_RDWR. En fermant d’un coup les « deux côté » de la socket (c’est-à-dire autant en lecture qu’en écriture), read() sort en retournant une erreur, ce qui permet de gérer le soucis.

En parallèle de ça, Armel Fauveau m’a conseillé via Twitter d’utiliser TCP keepalive, pour éviter d’avoir un tel thread de garbage collecting. Malheureusement, cette technique ne permet que de vérifier que la connexion est toujours établie entre le client et le serveur ; si le client est parti dans les choux (ou qu’il a simplement décidé de ne rien faire et de consommer une connexion pour rien), il n’y a aucune différence.

Mais la suggestion semblait censée, alors j’ai voulu creuser la question. Il avait raison, ce serait étonnant que rien n’existe au niveau réseau pour gérer les timeouts. J’ai donc trouvé, c’est au final assez simple : la fonction setsockopt() avec les options SO_RCVTIMEO et SO_SNDTIMEO. La littérature sur la question n’est pas débordante, mais le minimum vital est assez facile à trouver.

L’une des choses à laquelle il faut faire attention, quand on utilise ces options, est de ne pas les activer en continue. Imaginons que l’on place un timeout de 10 secondes, aussi bien en lecture qu’en écriture. Le serveur va passer le plus clair de son temps à attendre des données. Une fois qu’il les aura reçues, le compteur de timeout en lecture va repartir de zéro ; mais celui d’écriture continuera à tourner. Donc si le temps d’attente suivi du temps de traitement des données est supérieur au timeout d’écriture, la socket va être fermée alors que d’un point de vue applicatif tout se déroule comme prévu. De la même manière, après avoir reçu des données, le temps de les traiter puis d’envoyer un retour au client peut être plus long que le timeout de lecture.
Là, le truc est de placer le timeout sur la socket juste avant l’opération système bloquante (read() ou write()), puis de le retirer juste après. Ainsi, on s’assure que ce ne sont que les délais de communication client-serveur qui sont pris en compte par les timeouts.

Je viens de mettre à disposition une version du code qui implémente cette évolution.

Au passage, les évolutions de FineDB sont listées sur le site du projet.
Pour ceux que ça intéresse, j’ai ajouté une commande PING au protocole, permettant de tester si une connexion au serveur est toujours effective. Je l’utilise dans l’outil en ligne de commande, pour tester automatiquement la connexion avant l’envoi d’une requête, pour reconnecter automatiquement si nécessaire (c’est une option désactivable).
Pour le reste, les commandes de base sont toutes implémentées, y compris le support des transactions. La bibliothèque cliente et l’outil en ligne de commande sont à jour par rapport aux fonctionnalités du serveur.

FineDB : Architecture interne

J’ai lancé le projet FineDB il y a quelques semaines. Je vais expliquer comment fonctionnent les mécanismes internes du serveur.

Pour info, le projet a un site dédié : finedb.org

Threads

Il existe 3 types de threads dans le serveur FineDB :

  • Le thread principal, qui crée les autres threads puis écoute les nouvelles connexions venant des clients.
  • Le thread d’écriture, dont le rôle est d’écrire dans le moteur de stockage les données qui doivent être écrites de manière asynchrone.
  • Les threads de communication. Ils gèrent les échanges avec les clients, en interprétant leurs requêtes. Les lectures et les écritures synchrones sont faites directement ; les écritures asynchrones sont déléguées au thread d’écriture.

Communication entre les threads

Il y a deux flux de communication nanomsg à l’intérieur du serveur.

Le premier est un flux de type “fanout” PUSH/PULL load-balancé. Le thread principal y envoie les files descriptors correspondant aux sockets des connexions entrantes. Nanomsg distribue automatiquement ces messages aux threads de connexion, qui peuvent ainsi entamer la communication avec les clients.

Le second est un flux “fanin” SOURCE/SINK. Les threads de communication l’utilisent pour envoyer des ordres au thread d’écriture.

Schéma

  1. Une socket TCP classique attend de nouvelles connexions entrantes.
  2. Connexion nanomsg utilisée pour transmettre les connexions entrantes aux threads de connexion.
  3. Les opérations de lecture sont faites directement sur le moteur de stockage.
  4. Les opérations d’écritures synchrones sont faites directement sur le moteur de stockage.
  5. Connexion nanomsg, utilisée pour transmettre les ordres d’écriture asynchrone au thread d’écriture.
  6. Le thread d’écriture accède directement au moteur de stockage.

État des lieux

Tout ce que j’ai décrit ci-dessus est déjà implémenté sous cette exacte forme.

Cette architecture est le fruit d’un choix. Le fait de passer par des threads qui prennent en charge les connexions entrantes est différent de la mode actuelle qui se base plutôt sur de la programmation événementielle mono-processus (à base de libevent ou libev).
Le grand avantages est une simplification importante du code, grâce à son découplage. Le thread principal écoute les nouvelles connexions, et une fois qu’elle sont établies il les envoie aux threads de connexion. Nanomsg facilite grandement cette communication, en jouant au passage les rôles de load-balancer et de file de message. Les threads de connexion ont pour seul tâche de lire les requêtes, les comprendre et les exécuter.
Il faut voir aussi que la programmation réseau mono-processus fonctionne bien tant qu’aucune opération bloquante ne peut ralentir l’ensemble des traitements. Dans le cas de FineDB, certains traitements (compression/décompression des données, accès au moteur de stockage) peuvent s’exécuter rapidement mais quand même trop lentement pour vouloir prendre le risque d’ajouter de la latence sur toutes les autres connexions.

L’inconvénient principal est que le nombre de requêtes traitées simultanément est égal au nombre de threads de connexion. Les connexions supplémentaires doivent attendre qu’une connexion se termine et que le thread devienne libre pour en prendre une nouvelle en charge.
Cet inconvénient est connu et assumé. Les threads sont très légers et un grand nombre peut être créé.

Évolutions futures

L’architecture actuelle va évoluer avec deux étapes.

La première, assez simple, consistera à gérer les connexions longues, pour les couper au bout d’un certain délai. Cela nécessitera un thread supplémentaire, qui parcourra à intervalle régulier la liste des connexions ouvertes et fermera celles qui n’ont pas montré signe d’activité depuis un certain temps.

La seconde sera plus complexe. Elle va servir à mettre en place les capacités de réplication (maître-esclave et maître-maître) de FineDB. J’en reparlerai dans un autre article, mais cela nécessitera d’ajouter encore un thread supplémentaire, qui gérera les interactions avec les autres serveurs.

Une autre piste d’amélioration serait de mixer programmation réseau événementielle et threads de traitement. Ainsi, le thread principal gérerait à lui seul les connexions ouvertes, mais confierait aux threads les opérations bloquantes. À voir si cela est nécessaire, en fonction des benchmarks que je vais mener ; cela demanderait une grosse réécriture du code.

Code défensif et sur-optimisation de code

Récemment, en faisant des revues de code avec mes développeurs, j’ai eu avec eux des discussions intéressantes que j’ai envie de partager sur ce blog.
L’un d’eux utilisait une pratique que j’appelle le code défensif. L’autre avait des idées d’optimisation de code qui étaient de la sur-optimisation. J’ai pris le temps de leur expliquer en quoi ces pratiques sont néfastes.

C’est quoi le code défensif ?

Ce que j’appelle du code défensif, c’est du code qui se défend contre le développeur qui l’a codé et qui va l’utiliser.

Je ne parle pas de l’écriture de bibliothèques qui sont destinées à être (ré-)utilisées dans des contextes variés et par des personnes très différentes. Dans ce cas-là, il faut au contraire prévoir le maximum de cas d’échec, et partir du principe que la bibliothèque est une « boîte noire » à laquelle on est susceptible de fournir des données complètement erronées.

Non, mon propos concerne du code qui « se connait » (si je puis dire). Un contrôleur qui appelle un objet métier dont il est le seul utilisateur. Une méthode publique qui appelle une méthode privée du même objet. Une fonction Javascript qui manipule un élément DOM de la page sur laquelle elle est présente.

C’est quoi le problème avec le code défensif ?

On pourrait se dire que c’est « mieux » de vérifier systématiquement les entrées d’une fonction. C’est plus « propre », c’est plus « sérieux ».

Mais il ne faut oublier que cela a deux impacts.
Le premier impact est peut-être le moins important : Toutes ces vérifications prennent du temps. Oh, pas grand-chose, bien sûr. Mais à force de vérifications inutiles, on finit par dégrader les performances d’une manière qui peut être perceptible.

Le second impact est bien plus grave : Ajouter des vérifications, cela veut dire ajouter du code. Plus de code veut dire plus de maintenance. Le code est moins lisible, il est plus difficile de faire évoluer la partie applicative, car il faut déjà la séparer du reste. Et quand on fait évoluer les fonctionnalités, il faut faire évoluer les vérifications initiales.

Encore une fois, il y a des situations où cela se justifie, et d’autres où c’est complètement inutile.

Quelques exemples

Imaginons un objet qui peut être utilisé de plusieurs manières. On va vérifier les entrées-sorties publiques, mais le développeur est censé savoir ce qu’il fait à l’intérieur de l’objet.

class AjouteEnBaseDeDonnees {
    public function ajoutEntier($i) {
        if (!isset($i) || !is_int($i))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($i);
    }
    public function ajouteFlottant($f) {
        if (!isset($f) || !is_float($f))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($f);
    }
    private function _ajoute($n) {
        // code défensif complètement inutile
        if (!isset($n) || (!is_int($n) && !is_float($n)))
            throw new Exception("Mauvais paramètre.");
        // ... ajoute la valeur en base de données
    }
}

Dans un cas comme celui-là, on voit bien qu’il est inutile de remettre dans la méthode privée les vérifications qui doivent être faite au plus tôt lors des appels à cet objet.

Autre exemple, une fonction Javascript qui manipule les éléments d’une page.

<html>
<head>
    <script type="text/javascript">
        // on part du principe que jQuery est chargé
        function afficheDate() {
            var panel = $("#panel-date");
            // code défensif inutile
            if (!panel[0]) {
                alert("Je ne peux pas écrire la date !");
                return;
            }
            panel.html((new Date()).toDateString());
            panel.show();
        }
    </script>
</head>
<body>
    <div id="panel-date" style="display: none;"></div>
    <div onclick="afficheDate()">Affiche la date</div>
</body>
</html>

On est d’accord, personne ne code réellement ce genre de chose. On fait des objets Javascripts qui sont proprement rangés dans des fichiers, qui sont eux-même nommés en fonction du namespace de ces objets. Mais ça reste valable dans l’idée.
Dans cet exemple, le code défensif est doublement inutile. Non seulement le développeur qui code la page est censé savoir ce qu’il fait, mais en plus jQuery gère les erreurs silencieusement. Honnêtement, pour que ce code ne fonctionne pas, il faut vraiment que le développeur ne teste pas sa page une seule fois.

La sur-optimisation de code

L’autre cas intéressant est la sur-optimisation. On comprend tous instinctivement qu’il ne faut pas optimiser inutilement. Mais il semblerait qu’on ne réussisse pas tous à sentir quand quelque chose relève de la sur-optimisation.

Pour commencer, revenons rapidement sur les problèmes générés par la sur-optimisation. Ils peuvent être de deux ordres.
Le principal soucis est similaire à celui du code défensif. Écrire plus de code implique de maintenir plus de code ; cela entraîne des difficultés pour le faire évoluer. Le code est plus dur à débugguer et plus lent à améliorer. Ces inconvénients ne peuvent pas être négligés, car à moyens terme ils peuvent devenir particulièrement coûteux.

De manière plus anecdotique, il faut voir qu’une optimisation inutile peut se révéler être contre-efficace. De la même façon qu’un pattern peut se transformer en anti-pattern quand il est mal utilisé, une sur-optimisation peut réduire les performances si elle est mal appliquée ou si elle s’exécute dans un contexte différent de celui qu’on avait en tête quand on l’a codée.

Prenons deux exemples :

  • Ajouter 15% de code en plus, correctement compartimenté, pour multiplier par 25 les performances d’affichage de toutes les pages d’un site web, c’est bien.
  • Ajouter 20% de code en plein milieu de l’existant, pour gagner quelques poussières sur des pages qui représentent moins de 5% du trafic global, c’est inutile.

Mais alors, comment reconnaître une sur-optimisation ?
Posez-vous deux questions simples :

  1. Quel pourcentage de code va être modifié ou ajouté ?
  2. Quel pourcentage d’amélioration puis-je espérer, par rapport au projet global ?

Ensuite il suffit d’appliquer quelques règles :

  • Plus de 33% de code modifié ou ajouté ? Pas bon.
  • Moins de 50% d’amélioration ? Pas bon.
  • Ça prend plus de temps d’optimiser que de coder la fonctionnalité ? Pas bon, sauf si le gain en performance est décuplé (x10 minimum).

Ensuite il ne reste plus qu’à adapter à chaque situation.

Optimiser trop tôt

Ceci est un cas particulier de la sur-optimisation. Comme le disait Donald Knuth :

Premature optimization is the root of all evil.

Les raisons pour lesquelles on peut chercher à optimiser trop tôt sont multiples :

  • L’habitude. On applique une recette telle qu’on l’a déjà appliquée maintes et maintes fois. La force de l’habitude fait qu’on ne réfléchit pas.
  • La volonté de bien faire. Après tout, vouloir optimiser un code pour le rendre plus performant, c’est une bonne chose, non ?
  • La facilité. Quand on a terminé un développement, il est parfois tentant de le peaufiner encore et encore, plutôt que de se pencher sur un autre développement.

N’oubliez pas que ce n’est pas parce qu’on a fait les choses plusieurs fois d’une certaine manière que ça en fait une « bonne pratique » − même si c’était une bonne chose à chaque fois jusque-là.

Mais pourquoi optimiser prématurément est si gênant ?

Tout simplement parce qu’il est plus important de mener un projet à son terme que de l’améliorer. Chaque chose en son temps : développer le projet d’abord, l’affiner ensuite.
À chaque fois qu’un bout de code est complexifié par une optimisation prématurée, on rend plus difficile les ajouts et extensions. Tant qu’un développement n’est pas complet, chaque optimisation qui y est apportée revient à s’ajouter des obstacles dans une course de fond.