Programmation de TOMUSS : retour d'expérience.

Au début TOMUSS était un simple tableau partagé dans lequel différents enseignants pouvaient remplir des cellules. Puis de nombreuses fonctionnalités non prévues ont du être ajoutées.

Ce document présente différents choix techniques et les analyses a posteriori.

TOMUSS a évolué et a pour objectif d'être utilisable par l'ensembles des enseignants de Lyon 1 pour l'ensemble des UE. Il supporte sans problèmes de UE avec 1500 étudiants et 45 colonnes.

J'ai développé TOMUSS seul, les choix techniques et l'organisation du code n'aurait pas été la même si le projet avait été fait à plusieurs.

Interface utilisateur.

Il est dans le navigateur web.

JavaScript coté client.

JavaScript est obligatoire si l'on veut faire une application interactive dans la navigateur.

Java n'a pas été choisi, parce que :

Cela serait à refaire, j'utiliserais un compilateur permettant d'écrire en Python et de générer du JavaScript. Évidemment, le code JavaScript doit rester performant.

Il y a très peu de problèmes de compatibilité JavaScript sur les programmes correctement écrit (sauf split). Les problèmes sont au niveau de DOM et des événements. Là aussi, les programmes simples (qui ne font pas des choses tordues) fonctionnent sans problèmes.

Le protocol data des URI permet à JavaScript de générer n'importe quel document dynamiquement.

<object type="image/svg+xml" data="data:image/svg+xml;utf-8,<svg></svg>"></object>
Malheureusement les restrictions pour des raisons de sécurité interdisent dans certains navigateurs (chrome) la modification du champs data d'un objet existant. Il faut donc générer l'objet total.

Quand j'ai commencé à écrire TOMUSS je ne savais pas que JavaScript permettait de faire des objets. Il manque donc une ou deux classes qui auraient bien simplifié le code.

Page unique.

Si l'on omet la page d'accueil permettant de choisir quel tableau on veut éditer, la seule page interactive est celle permettant d'éditer le tableau.

L'utilisateur devient donc rapidement à l'aise avec cet interface qui reste inchangé durant son travail.

Pour accentuer ceci, de nombreuses fonctionnalités sont présentées avec le même interface utilisateur :

Le choix a été fait d'afficher constamment l'ensemble des informations disponibles sans que l'utilisateur n'est besoin de le demander. Cela a été possible sans que cela devienne fouilli en :

Quand l'utilisateur déplace la cellule courante, les changements de valeurs sont mis en évidence dans l'interface afin de l'aider à savoir où regarder.

Bulles d'aides.

Tous les éléments de l'interface utilisateur possèdent une bulle d'aide.

Un conseil d'implémentation : ne pas utiliser les CSS, il faut gérer les bulles d'aide en JavaScript. Le positionnement et le contenu peut être fait défini intelligemment.

Des bulles d'aides à deux niveaux seraient bienvenues pour certaines fonctionnalité complexe.

Filtres.

Les filtres permettent de sélectionner des données. Leur syntaxe a été définie pour être abordable par les nouveaux utilisateurs tout en offrant d'innombrables possibilité pour les utilisateurs avancés.

La syntaxe des filtres est volontairement implicite afin d'être le plus facilement utilisable par un être humain. Par exemple <5 considère que l'on filtre des nombres et <d considère que l'on filtre des chaines de caractères.

On utilise évidemment ou et non |

Pour que les utilisateurs aient le moins possible d'apprentissages à faire. Les filtres sont utilisés dans de nombreux contextes :

Implémentation : Pour que les filtres soient performant une fonction JavaScript les réécrits sous la forme d'une fonction JavaScript qui est donc rapidement exécutable.

Edition dans le tableau.

N'ayant pas de formule à ralonge dans les cases ou de textes longs. La zone de saisie classique dans les tableurs n'a pas été faites afin de ne pas perdre de place.

La démarche initiale était de mettre un champ INPUT par case du tableau. Ceci n'est pas possible car cela cela prend trop de mémoire et de temps.

La solution actuelle est d'avoir un champ INPUT unique que l'on déplace par dessus les cases du tableau. Un autre avantage de cette approche est que si l'on tape du texte très vite est qu'aucun caractère n'est perdu quand on change de case.

Fonctions peu usités

On ne veut pas charger l'interface utilisateur en offrant des tas de fonctions qui n'intéressent que de très rares personnes. Pour ce faire, ces fonctions ne sont pas visibles sur l'interface, il est donc nécessaire de lire la documentation. Quelques exemples :

Architecture client/serveur.

Les clients de TOMUSS sont les navigateurs web.

Serveur web.

TOMUSS n'utilise pas un serveur web car c'est un serveur web. Il peut donc répondre instantanément aux requêtes car il est en attente de question et il a les informations disponibles en mémoire.

En fait, pour mieux égaliser la charge et pouvoir partager le travail entre plusieurs machines si nécessaire, il y a plusieurs serveurs. Le serveur TOMUSS gère l'affichage et la modification des tables. Les serveurs de suivi (un par semestre) gèrent l'extraction d'information de l'ensemble des tables d'un semestre. Aucun des serveurs n'a besoin de 2Go de mémoire.

Proxy Apache.

Les différents serveurs web utilisent des ports différents. Pour ne pas polluer les URL et simplifier les filtres qui autorisent les ports sur le campus, un serveur Apache proxy aiguille les requêtes vers le bon serveur TOMUSS.

Malheureusement, Apache se permet des choses. Il transforme les requêtes GET contenant %2F (/) et %3F (&) pour des raisons de sécurité. TOMUSS contient donc malheureusement une bidouille pour ne pas transmettre ces codes qu'il transforme en %01 et %02 qui sont des codes que l'utilisateur ne tape pas au clavier.

Apache MaxClient impose une limite sur le nombre de pages ouvertes simultanément car TOMUSS garde la session ouverte.

Serveur écrit en Python.

Le travail du serveur est tout à fait minimal puisque les clients ont leur indépendance. Un langage compilé n'est donc pas obligatoire.

J'ai choisi Python car :

JavaScript fait les calculs.

Afin d'alléger au maximum la charge du serveur, celui ci fait le minimum : vérification, stockage, diffusion.

Tout le reste est fait par le client. Ceci charge les machines utilisateurs mais cela permet une plus grande réactivité.

Toutes les pages de compte rendu comme les trombinoscopes, les feuilles d'émargement, les statistiques... sont générées complètement par JavaScript sans aucune intervention du serveur. Les pages contenant des tables peuvent elles-aussi être générées. En effet, le serveur n'envoit pas de page HTML mais des fichiers JavaScript permettant de générer le code HTML.

Le serveur ne faisant pratiquement rien, l'application peut fonctionner pour l'université complète sans aucun problème.

Cette manière de procédé soulève néanmoins un gros problème. Comme c'est le navigateur web qui fait les calculs de moyenne le serveur ne les connaît pas. Ceci est gênant quand le serveur doit faire des calculs statistiques sur de nombreuses UE. Ceci n'est pas possible actuellement car la code JavaScript qui calcule les moyennes n'a pas été reprogrammé en Python.

JavaScript coté client ne permet malheureusement pas (pour des raisons de sécurité), d'extraire des informations de pages web si cela n'a pas été prévu. Le client TOMUSS ne peut donc extraire automatiquement des informations d'une page web pour les intégrer dans un tableau TOMUSS. TOMUSS lui-même ne peut le faire car il n'a pas l'identité de l'utilisateur.

Authentification.

Le client est redirigé vers CAS pour obtenir un ticket. Le ticket attribué est indiqué dans l'URL, TOMUSS n'utilise pas de cookies.

Un répertoire contient un ticket actif par fichier. Ce répertoire est commun à tous les processus serveurs ceci permet de partager simplement les tickets sans avoir besoin de passer par un serveur de base de donnée.

L'authentification appelant un service externe, elle est traité de manière asynchrone afin de ne pas bloquer le système.

Communication client/serveur.

La solution de communication utilisée n'utilise pas les xmlrpc qui ne sont pas standard, qui sont inadaptés pour faire du pulling, qui chargent le serveur et qui sont lourdes.

Communication du serveur vers le client : la page web ne finie pas de se charger. Le serveur manipule directement les structures de données du client en lui envoyant du code JavaScript. Un ping est lancé toutes les minutes pour garder la liaison vivante.

Communication du client vers le serveur : une image contenant le feedback de l'action est créée, son URL déclenche l'action sur le serveur. Le serveur envoit donc une image au client pour indiquer le résultat de la requête, c'est le petit carré vert qui apparaît pour dire que la valeur est bien sauvegardée.

Les requêtes du client vers le serveur sont numérotés car :

Si une image ne peut être chargée, alors au bout d'un certain temps il faut demander à réessayer le chargement car certains navigateurs abandonnent définitivement le chargement. Pour savoir si l'image est chargée, on utilise l'événement onload.

Si le serveur fonctionne mais refuse d'accepter les requêtes c'est que le ticket est invalide. Il faut donc que le client relance une procédure de revalidation du ticket.

Envoit d'information du serveur vers le client. On veut résoudre les problèmes suivants :

Solution : Un ensemble de thread qui tout les dixièmes de secondes fusionne les demandes d'envoit et les fait.

Identificateurs.

A chaque chargement de page web, on affecte un numéro de page unique dans la table avec les informations de session. Ceci permet les reprises sans problèmes en cas de redémarrage.

Quand un utilisateur crée une nouvelle ligne ou colonne, il génère un identificateur en concaténant le numéro de page et un numéro d'entité créé.

Il ne peut donc jamais y avoir de conflit de création de ligne ou de colonne.

Quand on fait référence à des coordonnées dans le tableau, on fait systématiquement référence aux identificateurs et non à des index dans des tableaux.

Interrogation de serveurs.

On n'ouvre pas une connexion par requête pour ne pas surcharger le serveur distant.

Dans le cas de LDAP 3 connexions sont ouvertes qui doivent être utilisées à bon escient :

Les données.

Les données manipulées par TOMUSS sont des tables.

Stockage des données.

Pour n'avoir aucun problème :

Ceci nous assure de pouvoir récupérer les données en cas de problème.

De plus les données sont stockées sur deux disques différents.

__slots__

Le serveur doit stocker de très nombreuses cellules. Pour que les cellules prennent le moins de place possible l'attribut __slot__ a été indiqué. Il fige les attributs possibles pour les instances de cellule.

Stockage des données sous forme de programme.

J'utilise un serveur de base de donnée si les données que je traite :

Dans le cas de TOMUSS, les données tiennent en mémoire et un seul processus les modifie. L'utilisation d'une base de donnée ne peut donc que ralentir l'application sans apporter de plus value dans le cas de TOMUSS.

Les données sont stockées dans des fichiers qui sont des modules Python. Les fichiers sont ouverts en mode ajout, on ne peut donc pas perdre d'information. Le fichiers contiennent une suite d'appel à des fonction qui modifient les structures de données. En fait, le fichier contient un log des modifications.

La lecture des données se fait en faisant un import du module Python. Il n'y a pas besoin de faire un analyseur syntaxique. D'autres programmes peuvent relire les données en leur appliquant des traitements différents. Par exemple des statistiques ou bien une vérification de la logique. Ceci est possible car le fichier ne contient aucun a priori sur les structures de données.

Copie complète des données dans le client web.

Si on veut la meilleure interactivité dans le client web il faut que toutes les données soient copiées dans le navigateur.

Pour une grosse UE (1500 étudiants et 45 colonnes) cela représente 2.5Mo qui se chargent très rapidement sur le réseau local et en 5 secondes avec un liaison qui fait du 500Ko/s.

Cette solution est donc viable à l'heure actuelle. Il est possible de réduire la taille du fichier d'un facteur 3 mais cela ce fait au détriment de la lisibilité du code et du débuggage.

Les données sont envoyées sous la forme de programmes JavaScript. Elles sont donc instantanément utilisables. Ces programmes JavaScript sont presque 100% compatible Python, il y a seulement un problèmes de codage de l'unicode. L'idée originale était d'avoir une traduction des données Python vers JavaScript qui soit très rapide puisqu'elle utilise des fonctions Python ``cablées´´.

Les données étant copiées dans le navigateur la connexion réseaux n'est pas indispensable pour travailler sur le tableau en lecture et modification. Les données sont sauvegardée quand la connexion réseau est rétablie.

Quand les données sont modifiées sur le serveur elles sont imédiatement mise à jours dans les clients.

Programmation.

Nommage.

Pour ne pas s'y perdre il faut utiliser un nom unique pour représenter la même entité sous ses différentes formes.

Et bien sur, il faut prendre des noms suffisamment longs pour n'avoir aucune ambiguïté.

Template.

Pour TOMUSS les tables sont vides de signification. Les templates permettent d'associer à une table ou un semestre une certaine sémantique. On peut remplir automatiquement la table ou bien changer légèrement l'interface utilisateur.

Données définies de manière générale.

Ce sont des points faibles de l'implémentation dus au fait que l'objectif du programme initial était restreint.

Plugins.

Les plugin définissent les fonctions accessibles via HTTP. Un plugin est défini par :

Lorsque le serveur reçoit une requête, il l'envoit au plugin qui correspond le mieux.

Les droits d'accès ont malheureusement été définis extensivement dans l'objet plugin (is root, is an abj master, is a teacher, the password is ok). Ils auraient du être définie par un modèle fonction, par exemple : is_a_teacher and password_is_ok L'expression précédente construit une fonction qui sera évaluée pour déterminer si le plugin est activable. Ceci permet de faire évoluer les droits d'accès indépendamment des plugins. De plus, cela permettrai de modifier les droits d'accès aux plugin directement dans la table de préférences en dynamique.

Chargement dynamique.

Le chargement dynamique permet de changer des parties du système sans avoir besoin de l'interrompre. C'est un énorme gain de temps en développement.

TOMUSS fait du chargement dynamique pour tous les fichiers qui sont envoyés au navigateur ainsi que pour les templates et les plugins à la demande de l'administrateur.

Le rechargement d'un module Python fait réexécuter le code et réinitialise donc les variables du module.

Thread.

L'idée de départ était que le processus principal soit le seul pouvant modifier les structures de données et que les autres thread soient des lecteurs. Avec un tel programmation les verrous ne sont pas nécessaires.

La réalité à montré que ce n'est pas possible car plusieurs thread peuvent modifier les données en même temps. Il faut donc gérer des verrous.

Les méthodes des objets ne verrouillent pas, mais elles vérifient qu'elles sont bien appelées avec un verrou fermé. Ceci permet de détecter les bugs et de faire des sections critiques combinant plusieurs actions élémentaires.

Une thread par type de travail a été créée afin de simplifier la programmation et éviter tout problèmes en cas de lenteur réseaux avec les services extérieurs ou bien avec le client web. Chacune thread lit le travail qu'elle a à faire dans une file. Ceci nous assure que si un service est lent cela ne bloquera pas l'accès aux autres services, mais aussi que l'on ne submergera pas un même service avec de nombreuses requêtes simultanées.

Mémoire.

Les fuites mémoire sont toujours possibles. Une interface web a donc été créée pour naviguer interactivement dans les structures de données du serveur en production. Bien qu'elle n'ait été utile qu'une seule fois cette interface a permis de trouver rapidement une erreur.

Ce genre d'interface est réellement requis dans le cas de serveur contenant des structures de données complexes. Il permet aussi d'écrire des tests de régression externe à l'application.

Mail en cas de problème.

En cas de problème non prévu, un mail est envoyé à l'administrateur avec la pile d'appels des fonctions. Il faut bien sur faire attention à l'envoi de mail dans une boucle...

Ceci permet d'être très réactif en cas de problème.

D'autre part TOMUSS mesure le temps en le début de chargement d'une page et son affichage effectif. Il détecte les pages pour lesquelles l'utilisateur à abandonné le chargement. Si un utilisateur abandonne 4 chargements en une heure TOMUSS lui envoit un mail lui explique qu'il faut désactiver son anti-virus car dans 99% des cas c'est la source du problème.

Le mail envoyé à l'administrateur contient la pile d'appel des fonctions. Il appelle aussi la méthode 'backtrace_html' de toutes les variables locales de chaque fonction pour donner le contexte de l'erreur. Pour qu'une information soit affichée dans la pile, il suffit donc d'ajouter une méthode à la classe que le veut tracer.

Surveillance.

Pour le fun un visualisateur graphique temps réel de l'activité de TOMUSS a été réalisé. Ce visualisateur écrit uniquement en JavaScript et SVG ne charge pas du tout le serveur et permet de voir comment le système fonctionne en affichant les clients, les objets et les méthodes activées.

Il met en évidence les temps d'attente de services extérieurs. Normalement c'est invisible sauf si le services extérieur est bloqué.

Ce visualisateur, outre le fait qu'il soit beau a permis de trouver plusieurs problèmes de fonctionnement dans le serveur. En effet, tout comportement aberrant qu'il soit logiciel ou bien humain est immédiatement visible et interprétable.

Spécialisation du code.

Plutôt que de prévoir des détournements partout dans le code pour l'adapter à son environnement d'exécution (test de régression, démonstration, Lyon 1...) ce qui aurait alourdi le code ; la spécialisation du code est faite lors du chargement du module de configuration qui va patcher les autres modules pour leur faire réaliser les bonnes fonctions.

On regroupe donc tous les changements nécessaires dans un seul fichier, ceci est plus clair et aussi plus rapide car il n'y a pas de test en cours d'exécution.

Tests de régressions.

Comme dans toute application il est obligatoire de faire des tests de régression.

Pour tester le serveur un faux client fait des requêtes et teste le résultat. Malheureusement ceci ne peut ce faire qu'en remplaçant les services externes utilisés par le serveur par des faux services retournant toujours la même chose. Le serveur dans son ensemble ne peut être testé à moins de réécrire de faux serveurs externes.

Pour tester le client. Un fonction javascript appelle les fonctions haut niveau de l'interface utilisateur et vérifie leur action.

Une tentative de test de l'application web via une application externe au navigateur a été tenté mais les résultats sont trop aléatoire. Ceci a été fait en lançant le navigateur (quelque soit son type y compris IE, via un terminal serveur) dans un serveur X11 dédié auquel on envoit des événements clavier et souris. Le testeur fait des copies de l'écran X11 et des traitements pour vérifier que l'on a bien ce qui est voulu. Les navigateurs ayant un comportement aléatoire ceci n'est pas possible.

Les échecs.

La localisation du code est inexistante.

Il aurait été bien d'avoir un nommage automatiques des entité HTML pour faire plus facilement des CSS. Mais comme le code était généré à la main...

Il ne faut pas faire de calcul au moment du chargement d'un module Python. Il faut faire une fonction d'initialisation afin que les initialisations soient faites dans le bon ordre.

Idem en Javascript : on ne sait pas dans quel ordre les scripts sont chargés (option defer inconsistante).

Les bulles d'aides sont incluses dans le code HTML, il ne faut pas car cela crée plein d'éléments inutilement. Comme pour le curseur, il faut une seule bulle d'aide dont le contenu sera mis à jour. Ceci sera facile à corriger car les bulles d'aides sont générées par du code JavaScript. Cela aura un autre avantage, les bulles d'aides contextuelles seront plus facile à implémenter.

Les colonnes et les lignes sont stockées dans des tableaux au lieu de dictionnaires. Ceci accélère le code mais le complique inutilement et est source de nombreuses erreurs d'indice et de traduction.

Difficulté de programmation DOM : on ne sait pas quand les tailles des objets affichés seront correctes car on ne sait pas quand ils sont affichés. Moralité : on ne dois faire les calculs de positionnement qu'une fois que la page est affichée, ou alors ne rien faire de dynamique en fonction des tailles optimums.


Thierry EXCOFFIER
Last modified: Thu Jun 23 13:34:07 CEST 2011