Code Story 2013

code_story_logoÀ l’occasion de Devoxx France, la seconde édition du concours Code Story s’est mise en route. L’année dernière, il s’agissait d’écrire une solution au problème Foo Bar Qix. Cette fois-ci, c’est différent : il faut écrire un petit serveur web qui sera capable de répondre à différentes questions, tantôt simples, tantôt plus compliquées… La phase 1 s’est terminée dernièrement : petit retour sur ce “coding marathon” d’un mois ayant eu plus de 130 participants !

What is Code Story ?

La première phase de Code Story s’est fortement inspirée de l’exercice eXtreme Startup : un serveur central interroge les différents participants avec des questions qui évoluent au fil du temps. Ce serveur simule un marché dont la demande change perpétuellement. Il n’est donc pas possible de savoir à l’avance les besoins du marché. Et notre application doit être modifiable rapidement pour pouvoir prendre en compte au plus vite ces nouvelles demandes.

Techniquement, ces demandes sont représentées par des requêtes HTTP. Dans l’exemple ci-dessous, notre application se devra évidemment de répondre OUI.


GET /?q=Es+tu+heureux+de+participer(OUI/NON)

L’affaire se corse quand les questions deviennent des calculs un peu plus complexes :


GET /?q=((1,1+2)+3,14+4+(5+6+7)+(8+9+10)*4267387833344334647677634)/2*553344300034334349999000

Choix techniques

Pour répondre au challenge, je suis parti sur un stack technique “classique”, histoire d’avoir un socle technique dans lequel j’ai confiance, tout en rajoutant de l’exotisme avec la version de Java 8 supportant les lambdas. Mal m’en a pris : bien que l’environnement où mon application a été déployée supportait Java 8, seul l’édition standard (et donc sans lambdas) était disponible. Le principal problème étant de ne s’en rendre compte qu’au premier déploiement : c’est à dire bien trop tard.

Leçon à apprendre : déployer au plus tôt.

Mon application web a été déployée sur Cloudbees. Elle tourne sur le RUN@Cloud dans un conteneur web. En plus, je profite de la partie intégration continue (DEV@Cloud), qui a l’avantage d’avoir une instance Jenkins. Cette instance a pour but de compiler l’application, lancer la batterie de test, effectuer le packaging et redéployer l’application sur la partie RUN@Cloud. Ainsi, une mise en production est maintenant réduite à une commande : “git push”.

Simple, mais terriblement efficace…

… même Tom aurait pu faire une mise en prod (source : http://lesjoiesdusysadmin.tumblr.com )

La règle des 3T : Test, Test, Test

Il est donc facile de déployer une nouvelle version, le tout rapidement : parfait pour répondre vite aux nouvelles questions. Mais comment assurer la non régression et le support des nouvelles demandes du serveur Code Story ?

C’est le rôle de tests qui couvrent l’ensemble des cas d’utilisation de l’application. Pour ce faire, une batterie de tests unitaires et fonctionnels sont écrits. Ainsi, à chaque nouvelle requête venant du serveur CodeStory, un nouveau scénario de tests représentant cette requête est systématiquement ajouté. Ces scénarios fonctionnels lancent un serveur web avec l’application, puis une requête http est envoyée sur ce serveur local, via le framework de test rest-assured. Je simule ainsi complètement le comportement du serveur de Code Story, mais directement depuis ma machine.

C’est tellement pratique que, pas une seule fois, je n’ai eu à lancer le serveur localement pour le tester à la main.


@Test

public void should_respond_non_quand_on_me_demande_si_je_repond_toujours_oui() {

    assertThat(get("/?q=Est+ce+que+tu+reponds+toujours+oui(OUI/NON)").asString()).isEqualTo("NON");

}

Au final, plus de 90% de l’application est couverte, les lignes non couvertes étant généralement du code… mort, code qui aurait dû être supprimé.

coverage

Au fil du temps, de nouvelles questions sont reçues par notre application web : de nouveaux tests sont donc ajoutés. Au début du concours, seulement une dizaine de tests étaient présent ; au final, c’est par une cinquantaine de tests que l’application est couverte.

trend

Le code est complètement couvert. Je suis sûr qu’une nouvelle mise en production n’entraînera pas de régression. Et même si régression il y a, l’ajout d’un test permettra de s’assurer qu’elle ne reviendra pas de sitôt…

C’en est fini des mises en production périlleuses…

On peut observer qu’à certains moments, le nombre de tests diminue. En effet, il s’agit de phases de refactoring où je supprime du code, ainsi que le test unitaire qui va avec. Et même plus simplement, je supprime des tests qui s’avèrent être des doublons : c’est bien de faire le ménage de temps en temps !

Refactoring

La couverture de tests laisse la possibilité de changer ces choix : le code “génial” d’hier peut devenir le “legacy” d’aujourd’hui… Que faire alors ? Vivre avec ? Mais il peut nous ralentir lors des évolutions. On peut aussi simplement faire un petit ménage de printemps…

Petite mise en situation :

Lors du concours Code Story, un ensemble de questions m’a donné du fil à retordre : l’étape de la calculette. Il s’agit d’opérations arithmétiques dont il faut fournir le résultat. Les premières opérations sont de simples additions : 1+1, 1+2, etc.


GET /?q=1+1

Cette difficulté est résolue via une regex bien choisie qui détecte le symbole “+” et extrait les deux chiffres : il ne reste plus qu’à retourner la somme de ces deux chiffres. Vient ensuite des multiplications : 1*1, 2*2, etc. Même punition : regex, extraction des chiffres, multiplication.


public static final String PRODUCT_PATTERN = "(\\d+)\\*(\\d+)";

public String giveResponse(final String toQuestion) {

    RegexExtractor extract = PatternExtractor.extractFrom(toQuestion).withPattern(PRODUCT_PATTERN);

    return Integer.toString(extract.theGroup(0).asInt() * extract.theGroup(1).asInt());

}

Là où ça se corse, c’est avec l’arrivé de calculs légèrement plus complexes à programmer : (1+2)*2 ou bien (-1)+(1). La multiplication des membres intervenant dans l’opération me pose problème, tout comme la présence des parenthèses. Une adaptation de mes différentes regex permet de me sortir de ces situations, mais le code devient compliqué, voire très compliqué.

Plus tard dans le concours, la réception de l’opération “1,5*1,5” me donne le signal d’alarme que j’aurais dû voir plus tôt : je viens de créer une usine à gaz qui résout difficilement un problème semblant simple. Les tests ne m’ont pas empêché d’écrire ce “micmac” de code. Par contre, ils me permettent de réparer mon erreur et de pouvoir expérimenter de nouvelles choses.

Un certain nombre de participants sont partis sur de l’évaluation d’expression par un interpréteur, comme Groovy. J’ai essayé de faire pareil : trente minutes plus tard, ma calculatrice était remplacée par une simple invocation au shell Groovy, qui s’est chargé d’effectuer les calculs à ma place, le tout sans perdre de fonctionnalités.


new GroovyShell().evaluate("def q=" + calcul);

Les tests m’ont donc permis de revenir sur mes choix.

Jajascript

La dernière étape – la plus dure pour moi – était l’exercice Jajascript. A partir d’une liste de commandes qui ont une heure de départ, une durée, un prix, il faut trouver le planning adapté générant le plus gros gain, sans que les commandes ne se superposent. Ma première version en Java était trop complexe : trop de code pour résoudre un problème qui, ici encore, me semblait simple.

J’ai essayé de réécrire une nouvelle version, mais cette fois utilisant Scala et la récursivité :


def optimize(commandes: Seq[CommandeScala], accumulateur: PlanningScala = new PlanningScala(Seq())): PlanningScala = {

    if (commandes.isEmpty) {

        accumulateur

    } else {

        val tousLesPlanningsPossibles = commandes.map(commande => optimize(commande.commandesCompatibles(commandes), new PlanningScala(accumulateur.commandes :+ commande)))

        tousLesPlanningsPossibles.maxBy(_.gain)

    }

}

Cette nouvelle version est beaucoup plus compacte que la précédente. De plus, mixer du Java avec du Scala, dans le même projet, s’est fait très facilement via un plugin Maven. Cette version aurait été suffisante si l’équipe de Code Story n’avait pas eu (la mauvaise ?) idée de tester les performances de l’algorithme. Les requêtes reçues à ce moment-là demandaient un planning à partir de dix commandes… puis cents commandes… et cela jusqu’à cent milles commandes, avec un temps de réponse imposé de moins de trente secondes.

La plus grande difficulté sur cet exercice est de se rendre compte que le problème, ce n’est pas les structures utilisées (ArrayList ou LinkedList ?), ou bien les capacités de la machine (256Mo ou 512Mo de Ram ?), sur laquelle l’application tourne, mais bel et bien l’algorithme. Et trouver le bon “algo” qui résout ce problème m’a causé quelques soucis…mais ça, c’est une autre histoire.

Récapitulatif

http://cloc.sourceforge.net v 1.56 T=0.5 s (66.0 files/s, 3372.0 lines/s)
-------------------------------------------------------------------------------
Language     files      blank     comment    code
-------------------------------------------------------------------------------
Java            30       285         111     1112
Scala            2        26          23       76
XML              1         5           0       48
-------------------------------------------------------------------------------
SUM:            33       316         134     1236
-------------------------------------------------------------------------------

Mon Code Story : 23 jours de code, 156 commits, 51 tests, 80 déploiements, 1112 lignes de code Java, 76 de Scala, 135 participants, 4 consultants de Soat dans l’aventure, dont 3 qui passent à la phase suivante, et surtout 2 organisateurs à l’origine de tout ça : David Gageot & Jean-Laurent de Morlhon. Merci à eux !

Rendez-vous pour la phase 2, le 21 février, chez Google.

Nombre de vue : 34

AJOUTER UN COMMENTAIRE