Site icon Le blog Tech d'Indy

Exemple article

  • The interesting properties of pure functions

    The interesting properties of pure functions
    • The interesting properties of pure functions

    • Télétravail : comment trouver son curseur

      “Tu sais pas quoi ? Je viens au bureau mardi !”

      Ou : où, comment et pourquoi télétravailler.


      La question du rapport entre un pangolin et le cours de l’action des GAFAM peut vous sembler rhétorique aujourd’hui, à tête reposée et nez récuré (maintes fois). Mais il y a 3 ans à peine, on vous a peut-être ri au nez quand vous avez tenté subtilement d’aborder la question de l’intérêt de vous avoir équipé d’un pc portable si celui-ci est systématiquement accroché au même bureau — et ce, au bureau justement —

      Alors forcément aujourd’hui en parlant de télétravail et vu l’Histoire, on pourrait croire que je m’attaque vaillamment à l’invention de l’eau tiède. Tout a été dit. Tout ? Non ! Un village peuplé d’irréductibles gaulois résiste encore et toujours à l’envahisseur. Je discutais avec un ami pas plus tard qu’il y a 3 jours, un ami qui me disait que son but à court terme était d’apprendre à télétravailler.

      Drôle d’idée non ?

      Pourtant c’est un réel besoin pour lui, et ce malgré son doctorat en informatique, ses années d’expérience et la promotion des bonnes pratiques omniprésente dans son travail (le genre de gars à avoir toute sa CI rouge si il manque une Docstring, si vous voyez ce que j’veux dire).

      Parce qu’il ne considère pas le télétravail comme une évidence, et encore moins comme étant gagné d’avance.

      Et vous savez quoi ? Je pense sincèrement qu’il a raison. Et que j’ai eu tort.


      Les lecteurs attentifs de ce blog savent que j’ai toujours travaillé dans l’écosystème dit “startup”. Et pour moi le télétravail a toujours été un acquis. J’ai même commencé ma carrière dans un coworking avec mon CTO à ma droite et une autre société sur le bureau d’en face.

      Quoi ? Mais dans un coworking on est au bureau ! Ça n’est pas du télétravail !

      Alors si. Télétravail n’est pas synonyme de travail à la maison, et aujourd’hui on va se baser sur la définition suivante :

      💡 Télétravail === travail ailleurs que dans des locaux propres à l’entreprise de l’employé·e.

      Comme dans tous les jeux sociétaux, on n’a clairement pas tou·te·s les mêmes cartes. Et les privilèges, comme partout, ça change la donne. Ce n’est pas les gens coincés au RDC d’un immeuble borgne pendant que leur collègue prend le soleil du Sud sur la terrasse de son AirBnB de troisième confinement qui me diront le contraire. Et la visioconférence du lundi matin n’arrangera en rien les choses pour celleux pour qui l’Arcep utilise le terme “zone blanche” et qui disposent donc d’un débit internet proche du transport de disquettes 3,5” à dos de tortue.

      Alors attention, je fais le malin à utiliser de belles métaphores, mais bien sûr ce que je dis ici ne s’applique globalement qu’aux employé·e·s de bureau. Parce que pour la personne qui bosse à l’usine, le problème de débit internet pour la visio ça la fait doucement rigoler (sous le masque).

      Mais même si on se cantonne à ce que j’ai principalement connu jusqu’à maintenant, on a déjà du grain à moudre. À commencer par le café. Prenez-en un d’ailleurs, ou un thé. Ou une bière si vous n’êtes pas au bureau.


      Comme souvent, le télétravail peut fonctionner très bien comme très mal. Et je sais de quoi je parle, j’ai essayé les deux (pour toi public). L’important ici c’est de trouver son équilibre. Et cet équilibre n’est pas le même pour tout le monde ! Vous me direz, travailler systématiquement au bureau ne correspond pas à tout le monde non plus. Et vous aurez raison. Et je vous dirai oui ! Oui, et c’est l’intérêt majeur de la démocratisation du télétravail. Permettre à chacun de trouver l’équilibre idoine entre les différentes contraintes et avantages de chaque mode de fonctionnement.

      Parce que tout n’est pas rose en remote, et tout n’est pas gris au bureau. Loin de là.

      Je parlais d’équilibre pro / perso dans mon précédent billet, et le télétravail est un vrai bonus pour l’atteindre… quand bien utilisé. Sinon c’est une calamité qui va venir vous chercher au fond du lit (ou pire, au bar) pour le meet de 20h45.

      Il faut cependant bien admettre que plus de souplesse peut s’avérer précieuse sans porter préjudice à la sacro-sainte productivité (bien au contraire). Un exemple tout bête ? Votre créneau de livraison de colis s’étend de 8h lundi à 19h lundi (d’après) ? Ça serait dommage de poser 8 CP pour les 4 minutes d’interruption effective de votre travail non ? Un autre exemple : les rdv médicaux qui nécessitent très rarement 8h d’absence et ne se soldent que trop souvent par une journée de congé sans solde.

      À l’opposé du spectre on trouve malheureusement bien des désagréments. Le principal étant bien souvent lié au télétravail… à la maison. En effet, quand le télétravail est subi et non choisi, on est très rarement équipé correctement pour le faire chez soi. Très pragmatiquement, on peut citer l’équipement informatique (écran externe, périphériques dédiés, etc…) ainsi que le bureau et son fauteuil, un minimum ergonomique s’il vous plaît selon la médecine du travail. Mais le plus important dans le télétravail à domicile, ce qui peut transformer le tout en cauchemar, c’est bien un bureau. La pièce je veux dire. Rien de tel que de travailler sur son canapé ou dans son lit conjugal pour déclencher, subir puis haïr l’invasion de sa sphère personnelle par le boulot. La frontière jusqu’alors bien définie va finir par se flouter puis disparaître, et là adieu le droit à la déconnexion !

      Et n’oublions pas les coûts financiers imputés aux factures perso par les activités pro ! 8h par jour de consommation électrique, internet, et les 17 cafés quotidiens sur la petite machine domestique, ça finit par s’accumuler au bout de 52 semaines ! D’aucuns diraient que ces factures devraient être réglées par l’entreprise au prorata de l’utilisation professionnelle, et ils auraient bien raison…

      Vous me sentez amer peut-être, mais je suis pourtant un fervent défenseur du télétravail. Parce que j’ai un bureau dédié à la maison ! Les 50 minutes de train entre celle-ci à la campagne et le bureau d’Indy n’y sont pas pour rien non plus (coucou les pauvres parisiens prisonniers du RER).

      Mais ce n’est pas pour autant que je prône le full-remote, ou l’absence de locaux dédiés. Bien au contraire, je suis ravi de faire les trajets hebdomadaires et de voir les collègues ailleurs que sur mon écran ! Je suis ravi de partager tous ces moments et interactions ailleurs que sur Slack. Je suis ravi de pouvoir dessiner au tableau blanc pour présenter ma dernière idée farfelue ou qu’on m’explique un détail d’architecture. Je suis ravi de rencontrer la dernière promo d’indies, fraîchement débarquée en début de mois (ah oui, on recrute toujours “un peu” par ici !).

      Mais je suis aussi ravi d’éviter l’heure de pointe à Part-Dieu le lundi matin, je suis très content d’avoir la vue sur la forêt depuis mon poste, je préfère dépenser mes tickets-restau au restau qu’à la boulangerie du coin, et surtout, surtout, en bon geek du café le mien est meilleur que celui du bureau 🙃. Mais il n’est pas offert par Indy. Tout est question d’équilibre qu’il vous disait le mec au début de l’article.

      Vous aurez peut-être compris maintenant que je ne suis pas là pour plaider une organisation du travail ou l’autre, mais bien les deux ! Les deux à la fois, les deux parfois, l’une ou l’autre selon les besoins de chacun et chaque situation, en bonne intelligence. Car parfois il ne faut que ça : du bon sens. Et j’espère sincèrement qu’à la fin de la pandémie — d’ici 2043 — quand tout sera revenu à la normale, on n’aura pas l’idée saugrenue de faire un bond en arrière vers les normes obsolètes d’hier. Et je me prends à parier qu’on continuera à faire des pas de côté et à joyeusement démonter les mauvaises habitudes jusque là accumulées.

      Et toi alors ? Toi derrière ton écran sur ton canapé ou dans ton box. Tu paries avec moi ? Ou contre moi…

    • How to use VSCode debugger with multiple Docker services

      In my company, we use Docker and Docker Compose to run our Node.js services locally. Recently, I needed to configure and run the VSCode debugger on some of these services to debug a feature. There are a few things to know to achieve this, which I will share in this article with some basic examples.

      Before we start, here are the points that will serve as a guideline in this tutorial:

      • We want to keep using Docker and Docker compose to run our services, so that we have the proper environment for each of these services (environment variables, etc).
      • We do not want to touch the current docker-compose.yml which could, potentially, be used in the future to deploy our services in production.

      The Sample Application

      Let’s start by creating a first service. It is a simple web server that concatenates two strings, a first name and a last name, and returns the result. This service will live in a webapp/ directory at the root of the project.

      The Node.JS code

      webapp/package.json

      {
      	"name": "webapp",
      	"scripts": {
      		"start": "node src/server.js"
      	},
      	"dependencies": {
      		"express": "^4.16.1"
      	}
      }
      

      webapp/src/server.js

      const express = require('express');
      const app = express();
      
      app.get('/fullname', (req, res) => {
      	const firstName = req.query.firstNme;
      	const lastName = req.query.lastName;
      	res.send(`${firstName} ${lastName}`);
      });
      
      app.listen(8080, () => console.log('Listening on port 8080...'));
      

      webapp/Dockerfile

      FROM node:16
      
      WORKDIR /usr/src/app
      
      COPY package*.json ./
      
      RUN npm install
      COPY . .
      
      EXPOSE 8080
      CMD [ "node", "src/server.js" ]
      

      webapp/.dockerignore

      node_modules
      npm-debug.log
      

      The Docker configuration

      Now that the application code is written and the Dockerfile created, we can add a docker-compose.yml file at the root of the project.

      docker-compose.yml

      services:
      	webapp:
      		build: ./webapp
      		ports:
      			- "127.0.0.1:8080:8080"
      

      Let’s start the service.

      docker-compose build
      docker-compose up -d
      

      If you go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar, you should see the string undefined Bar, which is the unexpected behavior we will debug.

      Debugging the Application in Docker with VSCode

      The debugger command

      To allow the future VSCode debugger to attach to the Node service, we need to specify it when we start the process by adding the --inpect flag.

      Simply using --inspect or --inspect=127.0.0.1:9229 is not sufficient here because we need the 9229 port to be accessible from outside the service, which is allowed by the 0.0.0.0 address. So this command should only be used when you run the debugger in a Docker service. Otherwise, you would expose the port and the debugger to anyone on the Internet.

      webapp/package.json

      {
      	"name": "webapp",
      	"scripts": {
      		"start": "node src/server.js",
      		"start:docker:debug": "node --inspect=0.0.0.0:9229 src/server.js"
      	},
      	"dependencies": {
      		"express": "^4.16.1"
      	}
      }
      

      The Docker configuration

      Following our guideline, we do not modify the initial docker-compose.yml but create a second one that extends the first one. We will use the [f flag](https://docs.docker.com/compose/reference/#use–f-to-specify-name-and-path-of-one-or-more-compose-files) of the docker-compose CLI to use them both.

      docker-compose.debug.yml

      services:
      	webapp:
      		command: [ 'npm', 'run', 'start:docker:debug' ]
      		ports:
      			- "127.0.0.1:8080:8080"
      			- "127.0.0.1:9229:9229"
      

      Then, to restart the service with debug mode enabled, you can use this command:

      docker-compose build
      docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
      

      The service is now ready to be attached to the VSCode debugger.

      Running the debugger with VSCode

      At the root of your project, create a new directory .vscode and add the following configuration file.

      .vscode/launch.json

      {
      	"version": "0.2.0",
      	"configurations": [
      		{
      			"type": "node",
      			"request": "attach",
      			"name": "Debug webapp",
      			"remoteRoot": "/app/src",
      			"localRoot": "${workspaceFolder}/webapp/src"
      		}
      	]
      }
      

      When adding a breakpoint, the remoteRoot and localRoot properties will match the file’s position in the VSCode environment and its location in the Docker service file system.

      You can now start the debugger on the webapp service. Open the debugging panel and select the Debug webapp option. Then click on the play button.

      The debugger is started.

      Add a breakpoint on line 6 and then go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar.

      The debugger stops on line 6 and we can see that the variable firstName is undefined. The problem comes from line 5 where this is a typo on the firstName parameter name.

      To close the debugger, click on the button with a red square.

      Debugging Multiple Docker Services

      The Node.JS micro-service

      To take this a step further, we will add another service, named micro-service, which will be called by webapp.

      First, copy and paste the contents of the webapp directory into another directory named micro-service.

      Then, in the webapp directory, install axios and update the code as follows.

      npm install axios
      

      webapp/src/server.js

      const express = require('express');
      const axios = require('axios');
      
      const app = express();
      
      app.get('/fullname', async (req, res, next) => {
      	try {
      		const { data: fullName } = await axios.get('<http://micro-service:8080/fullname>', {
      			params: req.query
      		});
      		res.send(fullName);
      	} catch (err) {
      		next(err);
      	}
      });
      
      app.listen(8080, () => console.log('Listening on port 8080...'));
      

      The URL used line 8 is based on the name of the Docker service defined in the next section.

      The Docker configuration

      Add the new service to your docker-compose.yml. Note that it uses a different port so as not to conflict with the webapp service.

      docker-compose.yml

      services:
      	webapp:
      		build: ./webapp
      		ports:
      			- "127.0.0.1:8080:8080"
      	micro-service:
      		build: ./micro-service
      		ports:
      			- "127.0.0.1:3001:8080"
      

      Then, in your docker-compose.debug.yml, add the new service as well. Note that the debugger port is also different from the first one.

      docker-compose.debug.yml

      services:
      	webapp:
      		command: [ 'npm', 'run', 'start:docker:debug' ]
      		ports:
      			- "127.0.0.1:8080:8080"
      			- "127.0.0.1:9229:9229"
      	micro-service:
      		command: [ 'npm', 'run', 'start:docker:debug' ]
      		ports:
      			- "127.0.0.1:3001:8080"
      			- "127.0.0.1:9230:9229"
      

      Now build and start the two services.

      docker-compose build
      docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
      

      Running multiple debuggers with VSCode

      The last thing to do is to add the configuration of the second debugger in launch.json.

      .vscode/launch.json

      {
      	"version": "0.2.0",
      	"configurations": [
      		{
      			"type": "node",
      			"request": "attach",
      			"name": "Debug webapp",
      			"remoteRoot": "/app/src",
      			"localRoot": "${workspaceFolder}/webapp/src"
      		},
      		{
      			"type": "node",
      			"request": "attach",
      			"name": "Debug micro-service",
      			"port": 9230,
      			"remoteRoot": "/app/src",
      			"localRoot": "${workspaceFolder}/micro-service/src"
      		}
      	]
      }
      

      Once the configuration is added, you can run the two debuggers for each service.

      Once both debuggers are started, add a breakpoint in each service and go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar. The application will stop successively on each breakpoint.

      Your VSCode debugger is now fully configured to work with your Docker services. Congratulations!

    • Tail call: Optimizing recursion

      Recursive functions are recommended to solve some problems, or imposed by languages like pure functional programming languages which don’t have for and while loops. But recursion may comes at a price.

      Let’s use recursion to sum the elements of an array and understand how to optimize it with tail call optimization.

      Note that I use JavaScript and although tail call optimization appeared in ES 6 specifications, most engines don’t implement it, including V8. Apple WebKit is one of the only ones where it is available. Other languages implementing tail call optimization are listed in the Wikipedia article about tail call.

      Naive implementation

      function sum(x) {
      	// Base case. The sum of no elements is 0
      	if (x.length === 0) {
      		return 0;
      	}
      
      	// Add the first element to the sum of all the other elements
      	return x[0] + sum(x.slice(1));
      }
      

      It runs like this:

      Comes the call stack. The call stack keeps track of where the interpreter is in a program that calls multiple functions. When the program calls a function, the interpreter adds that function on the top of the call stack. When the function is finished, the interpreter takes it off the stack.

      Basically that’s how it computes sum([1, 2, 3]):

      [] is the base case. It returns 0. The interpreter has stacked 4 frames. Now it can start unstacking. With bigger arrays (about 10 000 elements depending on configuration), the call stack could reach its maximum size, crashing the program.

      Tail call to the rescue

      Tail call means that the final action of a function is a function call.

      In the previous implementation, the final action of the function was an addition.

      Let’s apply tail call. To do that, you have to add an accumulator argument to the function to save the partial sum. Using an accumulator is often required to apply tail call.

      function sum(x, acc = 0) {
      	// Base case: no more elements, returns the accumulator
      	if (x.length === 0) {
      		return acc;
      	}
      
      	// Call sum() without the first element
      	// Add the first element to the accumulator
      	// The recursive call to sum() is now the final action of the function
      	return sum(x.slice(1), acc + x[0]);
      }
      

      It runs like this:

      The interpreter doesn’t have to go back to the previous sum() call. It only has to go back to the original caller sumCaller(). Thereby, the call stack doesn’t need to keep track of all calls to sum(). That allows an optimization called tail call optimization. Interpreters that implement tail call optimization will only use one frame to keep track of the current call to sum().

      With tail call optimization, the call stack is like this:

      Now, even with bigger arrays, the call stack will only use one frame to compute sum().

      Be careful using recursion and think about tail call optimization.

    • Accessibility

      Accessibility

      Foreword

      This is no techie article but it does indeed talk about tech.

      Don’t expect code blocks below, but I trust I’ll broach some important thinking points. I’m also no definitive expert on the subject, but I have always found the discussed issue to be dramatically important in our ecosystem.

      So please bear with me!

      Let’s play a game

      Imagine if you will that you’re surfing merrily on your favorite handheld device on a warm autumn afternoon. Battery chock-full of energy, 4G (5G even?) pouring kitten photos on your nice 90Hz OLED screen, fluid interface anticipating your every needs.

      And all is well.

      Now. Now, imagine that the sun suddenly shines through the low-hanging clouds and glares over your screen.

      You’re handicapped.

      Imagine your ISP decided you did not give them enough kitten-money and throttles your connection.

      You’re handicapped.

      Imagine it’s now winter and real darn cold. You’re wearing gloves, and the touch-screen doesn’t register your fingers.

      You’re handicapped.

      You get a notification. But your phone’s vibrator is deactivated. Lucky for you, it does ring. Unlucky for you, you work in a loud factory.

      You’re handicapped.

      BUT WAIT!

      I’m no handicapped person I hear you cry out, slightly worried.

      Well, you yourself might not be disabled, but you definitely will be handicapped from time to time, maybe less than others, maybe more, definitely for different reasons.

      What of it?

      Accessibility isn’t a buzzword or a fad. Accessibility isn’t just another metric or a Lighthouse report entry. Accessibility is actually one of the founding principles of the Internet (and maybe the most important one).

      <aside> ℹ️ Don’t quote me on that. !! You might want to quote the Contract for the web’s principles 1 though.

      </aside>

      (huge) list of 584 companies supporting Contract for the web

      All those guys and girls must sure know what they’re talking about.

      And what do we find there first? Right at the top?

      Ensure everyone can connect to the internet

      And just a bit further?

      Make the internet affordable and accessible to everyone

      Although these two principles are aimed respectively at governments and companies, I do believe that everyone is part of the Web (World-Wide, remember?) and should act accordingly. And it has nothing to do whatsoever with our personal mental and / or physical abilities.

      Accessibility is not about handicap. It is not about disability. It is no luxury. It’s the basis of an ever-expanding global network that cannot be allowed to leave anyone behind. It might sound cheesy, overly philosophical or even utopian, but it has direct implications for anyone building (for) the Web and potentially dire repercussions for anyone using it.

      1. Net neutrality is no joke
      2. No-one is immune to being handicapped
      3. Accessibility is a duty of those building tools online
      4. Accessibility is the standard and not the other way around
      5. Building accessible platforms means building better platforms tech-wise
      6. It’s no ethical conundrum, but it is an ethical imperative

      Let us dive deeper then.

      1. Net neutrality

      This one might surprise you, as it’s not commonly associated with handicap, but it might just be as important for this topic. Indeed, as we thrive not to discriminate handicapped users, we also must fight against data discrimination.

      If you’re throttled by your ISP, you’re not going to be able to use the Web normally.

      Let’s never give up on this one. It affects every single person as independent decentralized internet access is still somewhat of a utopia.

      I won’t go into too much details here, as so many much more relevant and informed people are fighting everyday for it.

      Do go support them though!

      2. Everyone is liable to handicap (yes even you)

      Disability and handicap are two very different concepts. If a disabled person might be handicapped most of the time, a perfectly able person will be handicapped from time to time. As evolved as we may like to think our society is, able-ism is still very present on- and off-line. And it does not stop at too few wheelchair-accessible stores and homes.

      It does not stop at allowing visually impaired people to use your website either. In this wonderful diversity of humans all around the disc globe, everyone has different needs and different ways of consuming the Web. Be it by aid of a screen-reader, by zooming every webpage to 200% or by using a slow reconditioned device inherited from your third sibling. You just can NOT assume everyone is on a broadband connection enabled desktop with a fullHD screen in good lighting conditions.

      An example so dumb it might seem trivial is the difference between touch-devices and mouse usage. You just don’t click the way you tap. And the other way round. And you must take both use-cases into account. Especially since the borders between devices seems to be fading (looking at you convertible laptops and stylus phones).

      3. As a developer it’s your job to build accessible platforms, not the user’s

      You might rarely see a web developer job offer containing explicit mention of accessibility, but it’s not because it’s not part a of the tasks. It’s because it’s an implicit prerequisite. As a developer, it is your duty to take it into account.

      If you love the Web (and I hope you do if you build it) you’re expected to take its philosophy into account. It’s right there in the name : World-Wide Web. World-Wide. Entire populations. The entire population.

      4. Accessibility is no afterthought. It’s the basis.

      It’s often thought that you can build stuff and make it accessible afterwards. That if you find some time at the end of the sprint you might either add aria attributes or enjoy a day off.

      Well… NO. If you don’t include it from the ground up, you are discriminating users. And discrimination is bad, m’kay? It’s no quick-win to block people from using your app.

      5. Virtuous circles and best practices

      Think about it. If you build stuff right on one aspect, chances are other parts of the system will benefit from it.

      Accessibility as a standard helps you build better tech.

      Taking time to think about the tabbing index of your page to allow users to navigate it using their keyboard can only improve the zoning and hierarchy of the page.

      Taking time to improve the contrast ratios of your UI will improve it’s design.

      Taking time to label your actionable elements will also improve the page’s SEO.

      Lowering your data payloads for low-bandwidth users will speed up page load for everyone.

      It’s win-win on so many levels.

      6. Ethics bro

      Ethics has always had bad press if any press at all. The trolley problem might spring to mind. Or NBC’s “The good place”. Or even the bible, perhaps?

      All things whatsoever ye would that men should do to you, do ye even so to them.

      Erm… English please?

      Do unto others as you would have them do unto you.

      That’s about it. As a Web-builder, you should create software that treats people ethically. Period. No maybe, no buts, no ifs, you just cannot expect people to use your software in ideal conditions and according to your own plan.

      You cannot predict the every use case, nor should you, you can just embrace accessibility as one of the basic building blocks of your app to ensure no one is left behind.

      Yes, even you.

      Footnotes

      [1] Principles for the web

      • “Ensure everyone can connect to the internet”.
      • “Keep all of the internet available, all of the time”.
      • “Respect and protect people’s fundamental online privacy and data rights”.
      • “Make the internet affordable and accessible to everyone”.
      • “Respect and protect people’s privacy and personal data to build online trust”.
      • “Develop technologies that support the best in humanity and challenge the worst”.
      • “Be creators and collaborators on the Web”.
      • “Build strong communities that respect civil discourse and human dignity”.
      • “Fight for the Web”.
    • Comment bétonner l’intégration d’une API

      Quand on développe une application d’une certaine taille, on fait généralement appel à des solutions tierces pour certaines fonctionnalités qui ne sont pas “coeur de métier”, ou tout simplement pour des besoins annexes à l’application (CRM et compagnie).

      Et si tout va bien, cette solution va nous fournir des API pour échanger avec elle.

      Parfois ces API fonctionnent parfaitement, et tout le monde est content. Spoiler alert : c’est loin d’être toujours le cas !

      Le but n’étant de taper sur personne, nous allons imaginer une API fictive, qui fonctionnera plus ou moins bien pour démontrer ce qui m’arrange.

      Voilà ce qui va se passer :

      On dispose d’une liste de villes pour lesquelles on a besoin de récupérer la météo.

      On va aller de l’implémentation la plus simple à la plus robuste.

      Si vous maîtrisez JavaScript, vous devriez connaître au moins jusqu’au niveau 2.

      Mais j’ai bon espoir de vous faire découvrir des outils sympa dans la suite, restez jusqu’au bout (le 7ème va vous étonner).

      Si vous voulez tester au fil de l’eau, tous les exemples sont disponibles ici : https://replit.com/@rsauget/blog-api

      Niveau 0 – naïf

      import _ from 'lodash';
      
      async function getWeather({ city }) {
      	// On ajoute un peu de délai pour se rendre compte de l'impact
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      }
      
      const cities = ['Lyon', 'Montpellier', 'Paris'];
      
      const results = {};
      
      for (const city of cities) {
        results[city] = await getWeather({ city });
      }
      
      console.log(results);
      // {
      //   Lyon: 'Soleil',
      //   Montpellier: 'Soleil',
      //   Paris: 'Pluie',
      // }
      

      On a ici une première version très simple : on boucle sur les villes demandées, et pour chacune on va récupérer le temps qu’il y fait. On agrège les résultats dans un objet.

      Dans un monde parfait, cette solution pourrait convenir.

      En pratique, vous aurez surement noté plein de défauts : c’est bien le but, et d’ici quelques minutes j’espère que nous en aurons traité la majorité !

      Le premier défaut de cette version naïve est d’effectuer une requête après l’autre, de façon séquentielle, comme si on avait besoin de la météo de la première ville avant de demander la météo de la seconde. On sent qu’il y a mieux à faire.

      Version 1 – exécution “parallèle”

      Le but n’étant pas de rentrer dans le détail de la gestion de l’asynchronicité en Javascript, vous me pardonnerez le raccourci de parler d’exécution parallèle.

      On va donc utiliser un Promise.all pour paralléliser les appels à notre service externe :

      import _ from 'lodash';
      
      async function getWeather({ city }) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      }
      
      const cities = ['Lyon', 'Montpellier', 'Paris'];
      
      // On construit l'objet résultant à partir de paires [clé, valeur]
      const results = Object.fromEntries(
        await Promise.all(
          cities.map(
            async (city) => [city, await getWeather({ city })],
          ),
        ),
      );
      
      console.log(results);
      // {
      //   Lyon: 'Soleil',
      //   Montpellier: 'Soleil',
      //   Paris: 'Pluie',
      // }
      

      Ca fonctionne bien parce qu’on a trois villes, et que notre API peut gérer 3 requêtes concurrentes les doigts dans le nez.

      Essayez d’en mettre 1000 pour voir : il y a de bonnes chances que l’API vous dise gentiment de vous calmer.

      Dans le meilleur des cas, ce sera une réponse HTTP cordiale : 429 - Too Many Requests.

      Mais ça peut passer par un timeout, une déconnexion impromptue (bonjour ECONNRESET), une erreur 500, ou même vous faire bloquer votre IP. Pas génial.

      Pour pallier ce problème, on va limiter le nombre de requêtes concurrentes.

      Version 2 – parallèle, mais pas trop

      On pourrait traiter les requêtes par paquets de 10 par exemple, mais l’idéal serait une fenêtre glissante : on en lance 10, et à chaque fois qu’une termine on en relance une, de sorte qu’on en ait toujours 10 qui tournent.

      Il y a une petite bibliothèque qui permet précisément de faire ça : p-limit (https://www.npmjs.com/package/p-limit)

      import _ from 'lodash';
      import pLimit from 'p-limit';
      
      async function getWeather({ city }) {
        await new Promise((resolve) => setTimeout(resolve, 10));
      	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      }
      
      const cities = _.range(1000).map(n => `Ville ${n}`);
      
      // Vous pouvez jouer avec la limite pour voir l'impact sur le temps d'exécution
      const limit = pLimit(10);
      
      // On va wrapper l'appel avec p-limit, qui gèrera quand déclencher la fonction
      const limitedGetWeather = (city) => limit(
        async () => [city, await getWeather({ city })],
      );
      
      const results = Object.fromEntries(
        await Promise.all(
          cities.map(limitedGetWeather),
        ),
      );
      
      console.log(results);
      

      Il y a d’ailleurs toute une famille de bibliothèques autour de p-limit : p-all, p-map

      Je vous invite à y jeter un oeil, elles sont bien pratiques et assez légères.

      En jouant sur le nombre de requêtes concurrentes qu’on autorise, on peut arriver à coller à peu près à ce que tolère l’API.

      Mais on peut faire mieux.

      Version 3 – parallèle juste ce qu’il faut

      La plupart des API explicitent leur politique de rate-limit dans leur documentation. Elle est généralement exprimée en nombre de requêtes par unité de temps, par exemple 10000 requêtes par minute.

      Et bien là encore, une bibliothèque qui gère ça très bien : bottleneck (https://www.npmjs.com/package/bottleneck)

      Contrairement à p-limit, bottleneck est assez costaude, mais gère tout un tas de situations :

      • “réservoirs” (on vous redonne 100 requêtes toutes les 10 secondes par exemple)
      • groupes : un ensemble de limiteurs similaires identifiés par une clé
      • distribué : on peut le connecter à un redis et le limiteur est synchronisé entre plusieurs services
      • politique de “débordement” : si on fait beaucoup plus de requêtes que la limite autorisée, les appels vont s’accumuler dans la file d’attente ; on peut décider de limiter la taille de cette file et de définir ce qu’on fait quand ça “déborde”
      • etc.

      On peut ainsi traiter à peu près tous les cas de figure. Mais pour aujourd’hui, restons sur l’exemple simple de 10000 requêtes par minute.

      import _ from 'lodash';
      import Bottleneck from 'bottleneck';
      
      // Vous pouvez jouer avec les limites pour voir l'impact sur le temps d'exécution
      const limiter = new Bottleneck({
        // 10000 requêtes par minute => 6ms entre chaque requête
        minTime: 6,
        // c'est généralement une bonne idée d'aussi limiter la concurrence
        maxConcurrent: 5,
      });
      
      // On wrap l'appel à l'API avec bottleneck, pour en limiter le flux
      const getWeather = limiter.wrap(async ({ city }) => {
        await new Promise((resolve) => setTimeout(resolve, 10));
      	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      });
      
      const cities = _.range(1000).map(n => `Ville ${n}`);
      
      const results = Object.fromEntries(
        await Promise.all(
          cities.map(
            async (city) => [city, await getWeather({ city })],
          ),
        ),
      );
      
      console.log(results);
      

      Grâce à bottleneck on peut être au plus proche du rate limit de l’API, et ainsi aller le plus vite possible sans se faire flasher.

      Version 4 – gestion d’erreur

      Je crois que c’est La Fontaine qui disait que rien ne sert de courir si c’est pour se gauffrer sur la première erreur venue (https://fr.wikipedia.org/wiki/Le_Lièvre_et_la_Tortue_(La_Fontaine)).

      Et bien c’est exactement ce qu’on est en train de faire. Si une seule requête échoue, elle entraîne toutes les autres dans sa chute, et on se retrouve sans aucun résultat.

      Ça peut être souhaitable vous me direz. Mais supposons pour l’exemple qu’on préfère savoir le temps qu’il fait dans 99% des villes plutôt que dans aucune.

      En plus, le Promise.all a un comportement qui peut surprendre : toutes nos requêtes vont s’exécuter, quoi qu’il arrive. Certaines peuvent faire des erreurs, ça n’arrête pas les suivantes pour autant. Et cerise sur le gâteau, l’erreur qui va remonter au final sera seulement la première de la liste.

      En résumé, en cas d’erreur il est difficile de savoir exactement ce qui s’est passé et ce qui a fonctionné ou pas. Ça va qu’on ne fait que des GET, imaginez si on était en train de passer des transactions bancaires…

      On peut mettre un try/catch dans notre fonction getWeather, et traiter chaque erreur individuellement. Ça laisse la possibilité de re-throw l’erreur en cas de panique.

      Mais si on souhaite gérer les erreurs dans leur ensemble, on a une autre fonction : Promise.allSettled(disponible à partir de node 12.10.0 https://node.green/#ES2020-features–Promise-allSettled).

      import _ from 'lodash';
      import Bottleneck from 'bottleneck';
      
      const limiter = new Bottleneck({
        minTime: 6,
        maxConcurrent: 5,
      });
      
      const getWeather = limiter.wrap(async ({ city }) => {
        await new Promise((resolve) => setTimeout(resolve, 10));
        // On a 1 chance / 10 d'avoir une erreur
        if (_.random(10) === 0) throw new Error('ECONNRESET');
      	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      });
      
      const cities = _.range(1000).map(n => `Ville ${n}`);
      
      // On zip les villes avec le résultat des requêtes pour faire des paires [ville, resultat]
      const results = _.zip(
        cities,
        await Promise.allSettled(
          cities.map(
            (city) => getWeather({ city }),
          ),
        ),
      );
      
      // Chaque résultat de Promise.allSettled est de la forme : { status: 'fulfilled', value: 'Résultat' }
      // ou { status: 'rejected', reason: 'Erreur' }
      // Ce qui avec le zip donne un results de la forme :
      // [
      //   ['Ville 1', { status: 'fulfilled', value: 'Soleil' }],
      //   ['Ville 2', { status: 'fulfilled', value: 'Pluie' }],
      //   ['Ville 3', { status: 'rejected', reason: 'ECONNRESET' }],
      //   ['Ville 4', { status: 'fulfilled', value: 'Pluie' }],
      // ]
      // _.partition va nous séparer la liste des résultats en deux listes, selon que la requête a réussi ou non
      const [successes, failures] = _.partition(results, ['1.status', 'fulfilled']);
      
      if (!_.isEmpty(failures)) {
        console.error(
          _.chain(failures)
            .groupBy('1.reason')
            .mapValues((results) => _.map(results, _.first))
            .value(),
        );
      }
      
      console.log(
        _.chain(successes)
          .fromPairs()
          .mapValues('value')
          .value(),
      );
      

      Promise.allSettled nous retourne la description du résultat de l’ensemble de nos requêtes, qu’elles aient réussi ou pas. Libre à nous de gérer les succès et erreurs comme on le souhaite.

      Maintenant, toutes les erreurs ne se valent pas.

      Si on demande une ville qui n’existe pas par exemple, on peut se contenter de retourner une météo undefined.

      Mais si le service est juste momentanément indisponible, on peut vouloir essayer une nouvelle fois.

      Version 5 – on persévère

      Un des grands préceptes Shadok nous enseigne que plus ça rate, plus on a de chance que ça marche (https://fr.wikipedia.org/wiki/Les_Shadoks).

      Dans notre cas, si l’API nous renvoie une erreur momentanée, ou indéterminée, on va vouloir retenter notre chance, jusqu’à ce que ça retombe en marche.

      Vous me voyez venir ?

      Bien vu, j’ai une bibliothèque pour ça : async-retry (https://www.npmjs.com/package/async-retry)

      Elle permet de réessayer une requête en erreur, avec de chouettes options :

      • délai entre les tentatives, avec un facteur si on veut l’augmenter de manière exponentielle, et également un facteur aléatoire si on veut
      • limite en nombre de tentatives et/ou en temps
      • annuler les tentatives restantes, en cas d’erreur fatale par exemple

      On peut ainsi faire quelque chose comme :

      import _ from 'lodash';
      import Bottleneck from 'bottleneck';
      import retry from 'async-retry';
      
      const limiter = new Bottleneck({
        minTime: 6,
        maxConcurrent: 5,
      });
      
      const getWeather = limiter.wrap(
        async () => {
          await new Promise((resolve) => setTimeout(resolve, 10));
          if (_.random(10) === 0) throw new Error('ECONNRESET');
          return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
        },
      );
      
      // On wrap l'appel avec async-retry, pour la gestion d'erreur
      const getWeatherRetry = async ({ city }) => retry(
        async () => getWeather({ city }),
        {
          // On retente toutes les 100ms dans la limite de 5 fois
          retries: 5,
          minTimeout: 100,
          factor: 1,
        }
      );
      
      const cities = _.range(1000).map(n => `Ville ${n}`);
      
      const results = _.zip(
        cities,
        await Promise.allSettled(
          cities.map(
            (city) => getWeatherRetry({ city }),
          ),
        ),
      );
      
      const [successes, failures] = _.partition(results, ['1.status', 'fulfilled']);
      
      if (!_.isEmpty(failures)) {
        console.error(
          _.chain(failures)
            .groupBy('1.reason')
            .mapValues((results) => _.map(results, _.first))
            .value(),
        );
      }
      
      console.log(
        _.chain(successes)
          .fromPairs()
          .mapValues('value')
          .value(),
      );
      

      Attention toutefois : c’est sympa de réessayer les requêtes jusqu’à ce que ça marche, comme un bourrin. Quand on GET de la donnée comme ici, il ne peut rien se passer de trop gênant. Si la requête fait des modifications en revanche, ça n’est pas toujours une bonne idée.

      Prenons un exemple au hasard : imaginez que Netflix fasse ça sur la requête de paiement de votre abonnement, et que celle-ci soit rejouée 10 fois. Eh ben ça va pas faire une soirée très “chill”, comme disent les jeunes.

      Il faut donc toujours se poser la question de l’impact de faire la requête plusieurs fois sur le fonctionnement du système, avant de mettre en place une telle logique.

      C’est aussi sympa pour l’API de mettre un délai exponentiel. Si elle est sous l’eau, vous lui laisserez ainsi une chance de reprendre ses esprits avant de vous répondre.

      Conclusion

      En combinant une logique de rate-limit avec une bonne gestion d’erreur, comme on a pu le faire avec le combo gagnant bottleneck + async-retry, on arrive à un code relativement tout-terrain.

      On l’utilise chez Indy pour certaines synchronisations un peu capricieuses, et ça tourne comme une horloge.

      Et pour finir sur une note plus sérieuse, gardez à l’esprit qu’il y a aussi des dev derrière les API sur lesquelles on aime râler, et que c’est très difficile, voire impossible, de fournir un service parfaitement fiable. Soyez compréhensifs, et raisonnables dans vos intégrations, ne faites pas les bourrins 😉

    • Récupérer les coordonnées dans un fichier pdf avec pdfjs-dist

      Contexte

      Chez Indy on fait de la compta :p. Nous devons donc produire différents documents (en pdf) qui seront soit télétransmis soit utilisés par nos clients afin de remplir les mêmes documents sur notre joli site des impôts.

      Presque chaque année, les documents changent (dans notre jargon on parle de millésime) et on doit mettre à jour notre générateur de document. Les documents sont mis à disposition par l’État et sont des PDF sans zone de texte clairement définie.

      Du coup nous prenons le pdf comme une image de fond et nous écrivons par dessus les bonnes données de chaque client. Il nous faut donc les bonnes coordonnées dans le pdf.

      Nous avons plusieurs problématiques.

      Différence entre PDFs

      La première difficulté est de voir la différence entre 2 millésimes. Nous utilisons pour ça l’outil diff-pdf (https://vslavik.github.io/diff-pdf/).

      Pour l’installer sur mac c’est facile (pour les autres OS je vous laisse regarder ici : https://github.com/vslavik/diff-pdf#obtaining-the-binaries) :

      brew install diff-pdf
      

      Ensuite on peut facilement voir la différence entre 2 pdf avec la commande suivante :

       diff-pdf --view 2033-sd_3330.pdf 2033-sd_3723.pdf
      

      La commande ouvrira une visionneuse (par contre c’est moche 😛).

      Récupérer les coordonnées dans un pdf

      Afin d’avoir les coordonnées, nous pouvons utiliser facilement des outils web. Pour les curieux, le lien vers le code final se trouve en bas de l’article.

      Nous commençons par créer le fichier html. Nous avons besoin d’un canvas. Nous allons aussi créer un span où on mettra les coordonnées.

      <div id="app">
        <div class="coord">Current: <span id="coord"></span></div>
        <canvas id="canvas"></canvas>
      </div>
      

      Un peu de css pour bien voir les positions.

      .coord {
        background-color: black;
        color: cornsilk;
        width: 135px;
        position: fixed;
      }
      

      Puis nous allons créer le fichier index.ts

      import { pdf } from './pdf';
      import * as pdfjsLib from 'pdfjs-dist';
      import pdfjsWorkerEntry from 'pdfjs-dist/build/pdf.worker.entry.js';
      pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerEntry;
      
      const canvas = document.getElementById('canvas') as HTMLCanvasElement;
      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
      
      const load = async () => {
        // le pdf est en base64 car le site des impôts n'accepte pas les requêtes cors
        const pdfDoc = await pdfjsLib.getDocument(pdf).promise;
        const page = await pdfDoc.getPage(1);
        const viewport = page.getViewport({ scale: 1.5 });
        const renderContext = {
          canvasContext: ctx,
          viewport: viewport,
        };
        canvas.height = renderContext.viewport.height;
        canvas.width = renderContext.viewport.width;
        await page.render(renderContext).promise;
      
        function getMousePos(e: MouseEvent) {
          var rect = canvas.getBoundingClientRect();
          return { x: e.clientX - rect.left, y: e.clientY - rect.top };
        }
      
        canvas.addEventListener(
          'click',
          function (e) {
            const pos = getMousePos(e);
      
            /// check x and y against the grid
            const [x, y] = viewport.convertToPdfPoint(pos.x, pos.y) as any;
            const currPos = [parseInt(x, 10), parseInt(y, 10)];
      
            document.getElementById(
              'coord'
            )!.textContent = `${currPos[0]}, ${currPos[1]}`;
          },
          false
        );
      };
      
      load();
      

      La ligne suivante permet de faire fonctionner correctement pdfjs-dist avec un worker.

      import pdfjsWorkerEntry from 'pdfjs-dist/build/pdf.worker.entry.js';
      pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerEntry;
      

      Le point important du code est à la ligne 33 avec viewport.convertToPdfPoint. Cette méthode permet d’avoir les bonnes coordonnées à l’intérieur du pdf. Elle va convertir le x, y du navigateur en coordonnées du pdf.

      Une fois que vous aurez cliqué sur une zone du pdf, vous allez voir les coordonnées dans le bloc noir en haut à gauche.

      Vous pouvez voir le résultat ici : https://typescript-qgcwq5.stackblitz.io

      Et voir le code là : https://stackblitz.com/edit/typescript-qgcwq5

      ❗ Le fichier pdf.ts est là car :

      • stackblitz (free) ne permet pas de déposer un fichier
      • impossible de récupérer les fichiers pdf via fetch directement par le site des impôts (ils bloquent les requêtes cors)
    • Les Caribous d’Indy 🦌

      Dans une startup on grandit vite, on sort rapidement de nouvelles fonctionnalités et de nouveaux projets. Les équipes tech & produit passent la majorité de leur temps sur ces nouveaux sujets pour toujours répondre au mieux aux besoins des clients.

      Il ne faut cependant pas négliger la maintenance de l’existant pour bâtir de solides fondations. Plus les fonctionnalités et projets s’accumulent, et plus le temps à accorder à la maintenance devient important. C’est pourquoi chez Indy nous avons mis en place ce que l’on appelle les Caribous 🦌.

      Le Caribou qu’est-ce que c’est ?

      Le Caribou est la personne en charge de la maintenance. C’est elle qui, en priorité, doit se charger de :

      • répondre aux tickets sur le support interne
      • débloquer ses collègues (au support notamment) en cas de problèmes
      • surveiller les alertes remontées par notre monitoring (ex: issues Sentry*) et alerter les équipes en cas de gros pépin
      • vérifier et régler les incohérences de données trouvées par nos algorithmes d’audit
      • s’assurer de la bonne santé de nos tâches asynchrones (ex: envoi des liasses à l’administration fiscale)

      Quel intérêt ?

      L’intérêt de dédier une personne à ces tâches est notamment de soulager les autres devs de l’équipe. En effet, avant que nous ne mettions en place ce système de Caribou, chaque dev avait l’obligation d’activer ses notifications slack sur nos différents channels d’alerte, et donc à tout moment pouvait être interrompu dans son travail. Et vous savez, comme moi, à quel point cela peut être difficile de changer de contexte régulièrement !

      Aujourd’hui, quand nous ne sommes pas Caribou, nous pouvons nous concentrer pleinement sur les tâches en cours, tout en sachant que les problèmes seront bien traités ! Bien sûr il n’est pas non plus interdit de donner un coup de main au caribou entre deux tâches 😉 !

      Chez Indy, et plus particulièrement dans mon équipe ScaleOps, le Caribou a été un véritable tournant dans l’organisation de l’équipe. Cela a permis à notre PM de se décharger d’une grosse part du temps qu’il passait à répondre aux tickets. Cela a aussi permis à tous les devs de l’équipe de se sentir investi équitablement dans les tâches de maintenance et de soutien aux autres équipes.

      Comment ça fonctionne ?

      Chez Indy, libre à chaque équipe de mettre en place ou non le système de Caribou et de la manière qu’elle le souhaite.

      Chez ScaleOps, nous avons établi un planning sur l’année pour attribuer le rôle de Caribou à tour de rôle chaque semaine à un dev différent. En cas de vacances ou d’absence, on s’organise dans l’équipe pour modifier l’ordre d’attribution.

      Le Caribou occupe le rôle pour une semaine entière, ce qui permet aux autres de vraiment se concentrer sur leurs tâches de développement, et en même temps ne dure pas trop longtemps, pour éviter au Caribou de se lasser des tâches de maintenance et de trop couper avec les sujets de roadmap en cours.

      Organisation du Caribou chez ScaleOps

      Dans mon équipe, le Caribou s’organise de la manière qu’il le souhaite. Nous avons cependant un ordre de priorité à accorder aux différents sujets.

      En effet, nous cherchons en priorité à régler les alertes remontées sur Sentry, car elles peuvent potentiellement impacter plusieurs utilisateurs (voire tous). Ensuite, nous nous attelons à traiter les tickets internes, qui concernent généralement un seul utilisateur.

      Pour ce qui est des audits (jobs qui tournent en tâche de fond afin de vérifier la cohérence des données de nos utilisateurs) nous regardons qu’ils tournent correctement qu’une fois par semaine, le volume d’erreurs étant faible nous n’avons pas besoin de plus.

      Après avoir vérifié tout ça, s’il reste du temps dans la journée du Caribou, celui-ci peut attaquer le développement des correctifs pour régler à la racine des problèmes remontés par les alertes Sentry, ou bien s’attaquer à de petits sujets de roadmap !

      Points d’attention

      Comme nous l’avons vu, le rôle du Caribou est d’être la vigie de l’équipe, et donc d’intervenir en premier en cas de problème. Par conséquent, s’il ne connaît pas assez le domaine concernant un de ces problèmes, il se doit de prévenir et de voir avec la personne compétente la meilleure manière d’aborder la situation.

      Notons aussi qu’il faut garder une bonne communication avec son PM, notamment lorsqu’il s’agit de problèmes nécessitant un développement plus conséquent. En effet, tous les problèmes ne sont pas à traiter dans l’instant. Il arrive que certains demandent plus de réflexion sur la manière de les solutionner, et donc il faudra sûrement intégrer ça dans la roadmap avec le PM.

      Un point très positif de ce système est qu’il permet de partager la connaissance facilement. Le Caribou aura à cœur de documenter un maximum les actions qu’il prend face à un problème (ex: méthode d’investigation et de résolution, différentes requêtes utilisées, etc.) dans ce que nous appelons la Caribase. Cela permettra aux prochains d’être plus efficaces face à un même problème. De plus, lorsque nous accueillons de nouveaux collègues, assister le Caribou est une excellente manière d’aborder la plupart des sujets de l’équipe !

      Et vous, comment fonctionnez-vous ? Que pensez-vous de notre système de Caribou 🦌 ?

    • Créer un index B-tree composé efficace,

      Créer un index B-tree composé efficace, un défi plein de pièges

      Nous avons tous eu à indexer nos bases de données à un moment donné.

      Nous avons tous cherché à indexer plusieurs champs avec des index composés à un moment donné.

      Et pourtant nos index composés peuvent avoir fait pire que mieux ou ne pas être à la hauteur. Nous allons ici avec un véritable use case voir la différence entre les bons et les mauvais index composés.

      Rappel du B-tree et idées reçues

      Etant donné l’étendu de la littérature sur les B-tree, nous ne nous étendrons pas trop dessus. Pour mémoire, le fonctionnement du B-tree est le suivant

      Pour lire la valeur 7, on va traverser l’arbre en partant du nœud racine en bleu, traverser la branche jaune à gauche et lire les valeurs en vert. La, l’index va nous donner directement l’adresse de l’enregistrement voulu.

      Dans le cas d’un index composé, l’index sera une succession d’index simples.

      Exemple de schéma si on créé un index B-tree sur les colonnes “A” et “B” (dans cet ordre).

      Par exemple avec la requête SQL

      CREATE INDEX "composed_index_name" ON "my_table" ("A", "B");
      

      On va créer l’arbre suivant

      Si maintenant, je veux la valeur A: 1, B: 5 voici le parcours (en rouge) qui va être effectué.

      Si on suit le trajet en rouge lors d’une requête : aucun doute c’est vraiment bien optimisé.

      De plus, en observant l’arbre, on voit apparaître un pattern assez connu et souvent documenté.

      Un index sur A et B permet aussi d’indexer des queries sur A uniquement

      Si, je reprend, l’exemple, si je veux tous les enregistrements ayant pour valeur de A à 1 il suffit de parcourir l’arbre en rouge ci-dessous.

      Relativement peu de flèches rouges, ça semble être effectivement une bonne astuce à première vue.

      Le piège du composed index

      Nous allons maintenant nous intéresser à l’index composé et essayer de voir comment il peut devenir un piège.

      Les données théoriques de nos index

      Pour ce faire, nous allons nous intéresser à de la donnée plus proche de la réalité que notre exemple théorique. On va stocker en base de données des exécutions de tâches s’exécutant trois fois tous les dix jours et échouant une fois sur trois.

      Ce qui nous donne les données suivantes :

      datestatus
      2022-01-01success
      2022-01-05error
      2022-01-10success
      2022-01-11success
      2022-01-15error
      2022-01-20success
      2022-01-21success
      2022-01-25error
      2022-01-30success

      Si nous avons les deux requêtes suivantes à indexer :

      • Récupérer un run pour une date donnée
      • Un compte des runs en échec entre deux dates. L’exemple s’appliquerait aussi si nous voulons tous les runs en échecs (sans compte), mais la récupération des runs serait coûteuse en temps, le count nous permet de mesurer uniquement le temps de parcours de l’index en excluant le fetch de données.
      # Requête 1
      SELECT * from runs WHERE date = <date>
      
      #Requête 2
      SELECT count(*) from runs WHERE date > <date_start> AND date < <date_end> AND status = 'error'
      

      Premier index

      En bon développeur devant une requête SQL lente, nous allons penser index.

      A partir de l’idée reçue vue au paragraphe précédent, nous allons préférer l’index suivant :

      CREATE INDEX "date_status" ON "runs" ("date", "status");
      

      Cet index, semble couvrir les deux queries avec un seul index.

      Intéressons nous maintenant à l’arbre que cet index va générer.

      Par rapport à ce à quoi nous nous attendions, celui-ci semble étrange.

      En effet, nous n’avons qu’un enregistrement par bloc de feuilles (en vert). Et il y a beaucoup de flèches.

      Nous allons maintenant exécuter nos deux requêtes :

      Récupérer un run pour une date donnée

      Si je veux récupérer le run du 21/01/2022, je vais exécuter le trajet suivant (en rouge).

      Le trajet semble plutôt bien optimisé. Peu de flèches rouges empruntées. Vérifions ça par la pratique. Nous allons utiliser la base de données témoin contenant 10 millions de runs avec un run sur 100 en erreur.

      ScénarioTemps d’exécutionTaille d’index
      Sans index+/- 2s0Mb
      index date_status+/- 2ms160Mb
      index date+/- 2ms120Mb

      On remarque que le temps d’exécution est très proche avec l’index date_status qu’avec l’index date uniquement.

      Le contrat est donc clairement rempli avec cet index sur cette requête.

      Seul bémol, la taille de l’index est bien supérieure à celle de l’index date uniquement on comprend aisément pourquoi en revenant à l’arbre. La surabondance de blocs et de flèches se paye sur la taille de l’index.

      Tous les runs en échec entre deux dates

      Les choses vont se compliquer dans ce cas. Dans notre exemple théorique, si je demande une plage de date équivalente à la moitié de l’amplitude de la base, on se retrouve avec le parcours suivant.

      Pour finalement récupérer un seul enregistrement, nous avons un parcours extrêmement complexe et coûteux.

      Si on valide par la pratique, en reprenant notre jeu de données de test, on va demander une plage de date correspondant à environ 500 000 runs (sur 10 millions de runs dans la table).

      ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
      Sans index+/- 2s+/- 2s0Mb
      index date_status+/- 2ms+/- 310ms160Mb
      index date+/- 2ms+/- 300ms120Mb
      index status+/- 2s+/- 100ms45Mb

      On observe effectivement que notre index composé ne fait pas mieux qu’un index uniquement sur le champ date.

      Il est important de noter que plus la fenêtre temporelle de requête est large, pire vont être les performances de cet index.

      A l’inverse, plus la fenêtre est étroite meilleur sera l’index.

      C’est donc un véritable camouflet pour le postulat de départ : un index composé sur A-B permet de requêter sur A de la même façon que un index sur A

      On observe même une valeur étrange, la requête est plus rapide avec l’index status que date_status. Pourquoi ? Tout s’explique avec les cardinalités. Nous avons 10 millions de runs dont 1% en erreur. Soit 100 000 runs alors que dans le parcours via date_status nous allons au final **parcourir 500 000 runs.

      Ce qui nous amène a une première grande idée générale, un index composé efficace est le plus souvent un index réfléchi pour une et une seule requête.

      Autre première observation, un index composé occupe environ le même espace que les index uniques qui le compose. Ca semble logique si on repense à la structure de l’arbre.

      Comment exploiter au mieux le composed index

      A partir de là, nous allons réfléchir aux solutions d’indexation qui s’offrent à nous et essayer d’en tirer quelques règles.

      Indexer chaque champ

      Contre toute attente, cette solution, sans aucune subtilité est souvent une bonne approche.

      CREATE INDEX "date" ON "runs" ("date");
      CREATE INDEX "status" ON "runs" ("status");
      

      SI nous reprenons l’exemple précédent, voici le résultat du bench.

      ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
      Sans index+/- 2s+/- 2s0Mb
      index date_status+/- 2ms+/- 310ms160Mb
      index date+/- 2ms+/- 300ms120Mb
      index status+/- 2s+/- 100ms45Mb
      index date + index status+/- 2ms+/- 300ms160Mb

      C’est encore un coup d’épée dans l’eau, on ne fait pas mieux que avec date uniquement et la taille des index a augmenté.

      Inverser l’index composé

      Plutôt que d’avoir un index sur date puis status, on va inverser et le créer sur status puis date.

      CREATE INDEX "status_date" ON "runs" ("status", "date");
      
      ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
      Sans index+/- 2s+/- 2s0Mb
      index date_status+/- 2ms+/- 310ms160Mb
      index date+/- 2ms+/- 300ms120Mb
      index status+/- 2s+/- 100ms45Mb
      index date + index status+/- 2ms+/- 300ms160Mb
      index status_date+/- 2s+/- 3ms120Mb

      Ca y est nous avons enfin trouvé un index qui fonctionne pour la deuxième requête. Mais nous y perdons sur la première requête.

      C’est bien la preuve que l’ordre des colonnes d’un index composé est très important.

      Si nous reprenons notre arbre, voici l’explication.

      Visuellement on voit tout de suite pourquoi l’index marche bien.

      Très peu de traits rouges, le chemin vers les feuilles est très direct.

      Nous pouvons en tirer une nouvelle bonne pratique un index composé efficace commence par les colonnes dont les valeurs ont les plus faibles cardinalités : le moins de valeurs possibles.

      Utiliser un index partiel

      Certains SGDB comme MongoDB et PostgreSQL permettent de spécifier des conditions sur un index. Ca devient particulièrement intéressant à combiner avec un index composé qui sera tunné précisément pour une requête. Ca permettra de diminuer l’empreinte de l’index sur l’espace de stockage.

      CREATE INDEX "partial_status_date" 
      ON "runs" ("status", "date")
      WHERE status = "error";
      

      Cet index va indexer runs de la même façon que l’index composé sur “status”, “date” mais uniquement pour les enregistrements qui matchent la condition du WHERE.

      ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
      Sans index+/- 2s+/- 2s0Mb
      index date_status+/- 2ms+/- 310ms160Mb
      index date+/- 2ms+/- 300ms120Mb
      index status+/- 2s+/- 100ms45Mb
      index date + index status+/- 2ms+/- 300ms160Mb
      index status_date+/- 2s+/- 3ms120Mb
      index partial_status_date+/- 2s+/- 6ms1Mb

      C’est une vraie bonne nouvelle !

      Pour 1Mb de stockage, nous avons un index qui va adresser très correctement la requête voulue.

      En conclusion, les bonnes pratiques de la création d’index composé

      Les leçons que nous avons tirées de notre exercice sont :

      • l’ordre des colonnes d’un index composé est important
      • un index composé vraiment efficace est souvent un index réfléchi pour une et une seule requête
      • un bon index composé occupe environ le même espace que les index uniques qui le compose
      • Dans le doute, on peut appliquer la règle suivante : un index composé efficace commence par les colonnes dont les valeurs ont les plus faibles cardinalités
      • Les index partiels sont souvent un bon compromis, pour une taille qui peut être très réduite ils permettent de multiplier les index.

      Maintenant j’espère que les index composés vous semblent beaucoup moins magiques.

      La stratégie de benchmark

      Pour valider nos futures hypothèses nous allons utiliser une table témoin contenant 10 millions de runs.

      Chaque enregistrement a le format suivant :

      ColonneTypevaleur
      dateDatedate de type auto increment
      statusTextun enregistrement sur 100 en échec (⚠️ dans les graphes on a un échec sur 3 pour que ce soit plus compréhensible)

      Les benchmarks ont été fait sur mongodb, mais peuvent s’appliquer à PostgreSQL aussi.

      Ressources

      Repository github contenant le benchmark : https://github.com/jtassin/blog-post-composed-index-1

      Partial index sur postgres

      Partial index sur mongodb

    • REX : comment mener une activité annexe en parallèle du salariat

      “Y’a pas que les ordinateurs qui sont multitâches”

      Ou, un retour d’expérience sur mes activités parallèles au salariat.


      Web developer who also develops film.

      👆Ainsi commence ma bio Twitter.

      Pour les moins anglophones d’entre nous, ça arrive, on pourrait le traduire maladroitement en “développeur informatique qui développe aussi des photos”.

      Forcément, au royaume des 140 caractères, la brévité est de mise et cette description de mon quotidien est un peu réductrice. Mais elle n’en reste pas moins très juste !

      L’informatique est mon métier ainsi qu’une partie de mes loisirs, et la photographie argentique est ma passion ainsi qu’une partie de mes activités professionnelles.

      Ce continuum travail / profession / loisirs, dans un monde de plus en plus fluctuant, de plus en plus (inter)connecté est une facette nouvelle d’un schéma du travail fondamentalement opposé à la construction traditionnelle de l’individu par sa profession et à fortiori par une carrière longue et unique.

      “freelances”, “slashers”, entrepreneurs individuels, auto-entrepreneurs, “micro-taskers”, autant de formes de travail à l’opposé du CDI à vie et qui semblent pourtant se pérenniser.

      Moi-même c’est mon 4ème contrat en tant que développeur web, dans autant d’entreprises différentes. Là où mon père passa quelques 35 ans de carrière dans l’informatique au sein de la même société. Et sans la photographie et l’auto-entreprise, je n’aurais jamais eu l’occasion d’écrire ces lignes, car je ne serais jamais devenu développeur.


      Revenons au début. Ou presque, ça prendrait trop longtemps.

      2008, pas trop mauvais au lycée, bac S et direction l’INSA Lyon pour étudier le génie mécanique et sans le moindre doute y dédier ma carrière.

      2010, pas trop mauvais en photo, que je pratique depuis quelques années, je décide de les publier sur internet, et de faire un blog. En bon ingénieur que je n’étais pas encore, j’en profite pour essayer de coder ce blog moi-même, et mets un pied dans WordPress.

      2011, pas trop mauvais en WordPress, autodidacte et très motivé, je crée mon auto-entreprise pour fournir des sites vitrines à des connaissances et y consacre une année blanche.

      2013, plus trop motivé par le génie mécanique, et bien plus intéressé par l’informatique, je me réoriente vers un DUT Informatique en année spéciale.

      2014, pas déçu par ce DUT, j’enchaîne sur un cursus d’ingénieur à nouveau. En alternance et en informatique cette fois-ci.

      2017, finalement BAC+5, je quitte l’alternance et rejoins une startup où je choisis le temps partiel pour continuer la photo sur mon temps perso.

      2019, toujours passionné de photo, je prends le statut légal d’artiste auteur.

      2019 aussi, je rejoins Indy, alors Georges.tech, et je conserve ce temps partiel.


      C’est bien beau tout ça, mais quand est-ce qu’on mange pourquoi on lit ça sur un blog tech ?

      Parce que chez Indy, la tech ne s’arrête pas au point-virgule final de nos fichiers (oui, ici c’est semi: ['error', 'always']). On recrute et travaille quotidiennement avec des humains, et c’est la base de notre philosophie. Pour paraphraser notre annonce pour le poste de développeur·euse :

      Chez nous, les valeurs ce n’est pas juste du wording à la mode. Elles font partie intégrante de notre culture. Nous recrutons des talents H.A.P.P.Y.*

      * : Humble, Attentionné·e, Passionné·e, Perfectionniste, Y-factor

      Facile à dire, non ?

      Pas si facile dans la vraie vie (IRL™️), et encore moins dans une entreprise en hyper-croissance qui voit arriver de nouveaux visages tous les mois !

      Pourtant mon quotidien de développeur chez Indy en bénéficie tous les jours, et je suis heureux de rendre la pareille dès que j’en ai l’occasion.

      💡 Car c’est réellement important.

      À tel point que dans les attentes de progression de carrière, on trouve ces valeurs au même niveau que l’architecture logicielle ou la connaissance de Node.js.

      Et tout à l’heure, j’ai répondu “J’encourage les équilibres pro/perso, fais attention à ce que chacun trouve sa place.” en face du Passionné de HAPPY.

      Et je me suis dit que oui, en effet, je n’étais pas qu’un développeur informatique dans une équipe technique.

      Allez, assez de bla-bla. Des faits !

      • Je suis en temps partiel.
      • Ou plutôt, en temps partiels, 80% / 20%.
      • J’utilise mes compétences acquises à Indy pour mon activité de photographe, entre autres.
      • J’utilise mes compétences de photographe chez Indy. Littéralement. J’y prends régulièrement des photos.
      • Mais pas que. Je participe aussi aux explorations produit, étant très bien placé pour connaître les besoins des indépendants.
      • J’ai un statut légal d’auteur photographe, donc techniquement j’ai deux activités professionnelles.
      • Je profite des refontes de mon site pro pour faire de la veille technologique.
      • Évidemment, j’utilise Indy (avec plaisir) pour ma compta photo 😜.
      • Et bien sûr je peux débugger les features sur un “vrai” compte !

      J’avais commencé mon contrat en 90% sous un format un vendredi sur deux, mais je me suis vite rendu compte que c’était une galère à organiser. Les collègues ne savaient jamais si j’étais au bureau ou au bar, je devais prévoir chaque mois les jours de présence en jonglant entre les différents mois selon le nombre de vendredis effectifs, bref, c’était pas idéal.

      Aujourd’hui (et depuis un bon moment du coup), je suis en 80% avec un format habituel jamais d’Indy le vendredi (oui ça rime). Sachant que ce n’est pas gravé dans le marbre chez nous, et que quand et si j’ai besoin d’adapter ça ne pose pas de souci. Par exemple un shoot photo qui tombe le mardi ne sera pas perdu, et je rattraperai ma journée le vendredi. Mais c’est quand même plus simple pour tout le monde quand c’est un jour fixe.


      D’ailleurs puisqu’on parle de simplicité, il faut bien avouer que le temps partiel — comme tous les compromis — n’est pas l’organisation la plus simple. Mais à mes yeux la balance finale penche clairement du côté du bénéfice malgré de nombreuses petites frustrations.

      Parce que oui, même si on met pas en prod le vendredi il se passe quand même des choses au bureau sur Slack | Github à la tech Indy. Je pense notamment aux évènements et rituels de l’équipe qui ont lieu le vendredi et auxquels je ne peux donc pas participer.

      💡 Rappel : pour les tech Indy, le vendredi après-midi c’est veille techno, montée en compétences et R&D, 10% du temps de travail y étant dédié.

      Et je rate donc le goûter du code, chouette moment de partage informel entre nous. Certes il est enregistré, mais j’ai rarement la disponibilité le reste de la semaine pour rattraper. Et c’est quand même moins gratifiant sans les interactions.

      Pour la même raison, je fais mon temps de veille le jeudi après-midi afin de profiter d’une fin de semaine agréable ce qui me met en décalage du reste de la vie tech sur cette période.


      Aujourd’hui je me considère chanceux. Chanceux de l’équilibre que j’ai construit entre pro et perso, chanceux d’avoir trouvé une société qui encourage la diversité et la passion, au sein d’une Société où c’est loin d’être gagné d’avance. Chanceux d’avoir un métier épanouissant en parallèle de mes passions.

      Je dis chanceux, mais ça n’a rien à voir avec la chance dans les faits. Ça relève de l’organisation, de la philosophie de vie et de la bonne volonté de chacun. C’est un cercle vertueux, moins connu que son antagoniste vicieux mais tout aussi potent.

      Et il faut bien dire que ça demande aussi pas mal de boulot ! Pas simple de jongler entre différents agendas. D’où l’intérêt de bénéficier d’une politique congés souple et de bien s’organiser !

      Et vous alors ? Des passions envahissantes ? Des loisirs professionnalisés ? Des projets à entreprendre ?

      Vous faites quoi à côté du travail ? Comment gérez-vous votre temps précieux ?

      Vous avez forcément quelque chose d’autre à investir et faire fructifier. Et dans le cas contraire, vous trouverez bien un jour 🙂

  • Télétravail : comment trouver son curseur

    “Tu sais pas quoi ? Je viens au bureau mardi !”

    Ou : où, comment et pourquoi télétravailler.


    La question du rapport entre un pangolin et le cours de l’action des GAFAM peut vous sembler rhétorique aujourd’hui, à tête reposée et nez récuré (maintes fois). Mais il y a 3 ans à peine, on vous a peut-être ri au nez quand vous avez tenté subtilement d’aborder la question de l’intérêt de vous avoir équipé d’un pc portable si celui-ci est systématiquement accroché au même bureau — et ce, au bureau justement —

    Alors forcément aujourd’hui en parlant de télétravail et vu l’Histoire, on pourrait croire que je m’attaque vaillamment à l’invention de l’eau tiède. Tout a été dit. Tout ? Non ! Un village peuplé d’irréductibles gaulois résiste encore et toujours à l’envahisseur. Je discutais avec un ami pas plus tard qu’il y a 3 jours, un ami qui me disait que son but à court terme était d’apprendre à télétravailler.

    Drôle d’idée non ?

    Pourtant c’est un réel besoin pour lui, et ce malgré son doctorat en informatique, ses années d’expérience et la promotion des bonnes pratiques omniprésente dans son travail (le genre de gars à avoir toute sa CI rouge si il manque une Docstring, si vous voyez ce que j’veux dire).

    Parce qu’il ne considère pas le télétravail comme une évidence, et encore moins comme étant gagné d’avance.

    Et vous savez quoi ? Je pense sincèrement qu’il a raison. Et que j’ai eu tort.


    Les lecteurs attentifs de ce blog savent que j’ai toujours travaillé dans l’écosystème dit “startup”. Et pour moi le télétravail a toujours été un acquis. J’ai même commencé ma carrière dans un coworking avec mon CTO à ma droite et une autre société sur le bureau d’en face.

    Quoi ? Mais dans un coworking on est au bureau ! Ça n’est pas du télétravail !

    Alors si. Télétravail n’est pas synonyme de travail à la maison, et aujourd’hui on va se baser sur la définition suivante :

    💡 Télétravail === travail ailleurs que dans des locaux propres à l’entreprise de l’employé·e.

    Comme dans tous les jeux sociétaux, on n’a clairement pas tou·te·s les mêmes cartes. Et les privilèges, comme partout, ça change la donne. Ce n’est pas les gens coincés au RDC d’un immeuble borgne pendant que leur collègue prend le soleil du Sud sur la terrasse de son AirBnB de troisième confinement qui me diront le contraire. Et la visioconférence du lundi matin n’arrangera en rien les choses pour celleux pour qui l’Arcep utilise le terme “zone blanche” et qui disposent donc d’un débit internet proche du transport de disquettes 3,5” à dos de tortue.

    Alors attention, je fais le malin à utiliser de belles métaphores, mais bien sûr ce que je dis ici ne s’applique globalement qu’aux employé·e·s de bureau. Parce que pour la personne qui bosse à l’usine, le problème de débit internet pour la visio ça la fait doucement rigoler (sous le masque).

    Mais même si on se cantonne à ce que j’ai principalement connu jusqu’à maintenant, on a déjà du grain à moudre. À commencer par le café. Prenez-en un d’ailleurs, ou un thé. Ou une bière si vous n’êtes pas au bureau.


    Comme souvent, le télétravail peut fonctionner très bien comme très mal. Et je sais de quoi je parle, j’ai essayé les deux (pour toi public). L’important ici c’est de trouver son équilibre. Et cet équilibre n’est pas le même pour tout le monde ! Vous me direz, travailler systématiquement au bureau ne correspond pas à tout le monde non plus. Et vous aurez raison. Et je vous dirai oui ! Oui, et c’est l’intérêt majeur de la démocratisation du télétravail. Permettre à chacun de trouver l’équilibre idoine entre les différentes contraintes et avantages de chaque mode de fonctionnement.

    Parce que tout n’est pas rose en remote, et tout n’est pas gris au bureau. Loin de là.

    Je parlais d’équilibre pro / perso dans mon précédent billet, et le télétravail est un vrai bonus pour l’atteindre… quand bien utilisé. Sinon c’est une calamité qui va venir vous chercher au fond du lit (ou pire, au bar) pour le meet de 20h45.

    Il faut cependant bien admettre que plus de souplesse peut s’avérer précieuse sans porter préjudice à la sacro-sainte productivité (bien au contraire). Un exemple tout bête ? Votre créneau de livraison de colis s’étend de 8h lundi à 19h lundi (d’après) ? Ça serait dommage de poser 8 CP pour les 4 minutes d’interruption effective de votre travail non ? Un autre exemple : les rdv médicaux qui nécessitent très rarement 8h d’absence et ne se soldent que trop souvent par une journée de congé sans solde.

    À l’opposé du spectre on trouve malheureusement bien des désagréments. Le principal étant bien souvent lié au télétravail… à la maison. En effet, quand le télétravail est subi et non choisi, on est très rarement équipé correctement pour le faire chez soi. Très pragmatiquement, on peut citer l’équipement informatique (écran externe, périphériques dédiés, etc…) ainsi que le bureau et son fauteuil, un minimum ergonomique s’il vous plaît selon la médecine du travail. Mais le plus important dans le télétravail à domicile, ce qui peut transformer le tout en cauchemar, c’est bien un bureau. La pièce je veux dire. Rien de tel que de travailler sur son canapé ou dans son lit conjugal pour déclencher, subir puis haïr l’invasion de sa sphère personnelle par le boulot. La frontière jusqu’alors bien définie va finir par se flouter puis disparaître, et là adieu le droit à la déconnexion !

    Et n’oublions pas les coûts financiers imputés aux factures perso par les activités pro ! 8h par jour de consommation électrique, internet, et les 17 cafés quotidiens sur la petite machine domestique, ça finit par s’accumuler au bout de 52 semaines ! D’aucuns diraient que ces factures devraient être réglées par l’entreprise au prorata de l’utilisation professionnelle, et ils auraient bien raison…

    Vous me sentez amer peut-être, mais je suis pourtant un fervent défenseur du télétravail. Parce que j’ai un bureau dédié à la maison ! Les 50 minutes de train entre celle-ci à la campagne et le bureau d’Indy n’y sont pas pour rien non plus (coucou les pauvres parisiens prisonniers du RER).

    Mais ce n’est pas pour autant que je prône le full-remote, ou l’absence de locaux dédiés. Bien au contraire, je suis ravi de faire les trajets hebdomadaires et de voir les collègues ailleurs que sur mon écran ! Je suis ravi de partager tous ces moments et interactions ailleurs que sur Slack. Je suis ravi de pouvoir dessiner au tableau blanc pour présenter ma dernière idée farfelue ou qu’on m’explique un détail d’architecture. Je suis ravi de rencontrer la dernière promo d’indies, fraîchement débarquée en début de mois (ah oui, on recrute toujours “un peu” par ici !).

    Mais je suis aussi ravi d’éviter l’heure de pointe à Part-Dieu le lundi matin, je suis très content d’avoir la vue sur la forêt depuis mon poste, je préfère dépenser mes tickets-restau au restau qu’à la boulangerie du coin, et surtout, surtout, en bon geek du café le mien est meilleur que celui du bureau 🙃. Mais il n’est pas offert par Indy. Tout est question d’équilibre qu’il vous disait le mec au début de l’article.

    Vous aurez peut-être compris maintenant que je ne suis pas là pour plaider une organisation du travail ou l’autre, mais bien les deux ! Les deux à la fois, les deux parfois, l’une ou l’autre selon les besoins de chacun et chaque situation, en bonne intelligence. Car parfois il ne faut que ça : du bon sens. Et j’espère sincèrement qu’à la fin de la pandémie — d’ici 2043 — quand tout sera revenu à la normale, on n’aura pas l’idée saugrenue de faire un bond en arrière vers les normes obsolètes d’hier. Et je me prends à parier qu’on continuera à faire des pas de côté et à joyeusement démonter les mauvaises habitudes jusque là accumulées.

    Et toi alors ? Toi derrière ton écran sur ton canapé ou dans ton box. Tu paries avec moi ? Ou contre moi…

  • How to use VSCode debugger with multiple Docker services

    In my company, we use Docker and Docker Compose to run our Node.js services locally. Recently, I needed to configure and run the VSCode debugger on some of these services to debug a feature. There are a few things to know to achieve this, which I will share in this article with some basic examples.

    Before we start, here are the points that will serve as a guideline in this tutorial:

    • We want to keep using Docker and Docker compose to run our services, so that we have the proper environment for each of these services (environment variables, etc).
    • We do not want to touch the current docker-compose.yml which could, potentially, be used in the future to deploy our services in production.

    The Sample Application

    Let’s start by creating a first service. It is a simple web server that concatenates two strings, a first name and a last name, and returns the result. This service will live in a webapp/ directory at the root of the project.

    The Node.JS code

    webapp/package.json

    {
    	"name": "webapp",
    	"scripts": {
    		"start": "node src/server.js"
    	},
    	"dependencies": {
    		"express": "^4.16.1"
    	}
    }
    

    webapp/src/server.js

    const express = require('express');
    const app = express();
    
    app.get('/fullname', (req, res) => {
    	const firstName = req.query.firstNme;
    	const lastName = req.query.lastName;
    	res.send(`${firstName} ${lastName}`);
    });
    
    app.listen(8080, () => console.log('Listening on port 8080...'));
    

    webapp/Dockerfile

    FROM node:16
    
    WORKDIR /usr/src/app
    
    COPY package*.json ./
    
    RUN npm install
    COPY . .
    
    EXPOSE 8080
    CMD [ "node", "src/server.js" ]
    

    webapp/.dockerignore

    node_modules
    npm-debug.log
    

    The Docker configuration

    Now that the application code is written and the Dockerfile created, we can add a docker-compose.yml file at the root of the project.

    docker-compose.yml

    services:
    	webapp:
    		build: ./webapp
    		ports:
    			- "127.0.0.1:8080:8080"
    

    Let’s start the service.

    docker-compose build
    docker-compose up -d
    

    If you go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar, you should see the string undefined Bar, which is the unexpected behavior we will debug.

    Debugging the Application in Docker with VSCode

    The debugger command

    To allow the future VSCode debugger to attach to the Node service, we need to specify it when we start the process by adding the --inpect flag.

    Simply using --inspect or --inspect=127.0.0.1:9229 is not sufficient here because we need the 9229 port to be accessible from outside the service, which is allowed by the 0.0.0.0 address. So this command should only be used when you run the debugger in a Docker service. Otherwise, you would expose the port and the debugger to anyone on the Internet.

    webapp/package.json

    {
    	"name": "webapp",
    	"scripts": {
    		"start": "node src/server.js",
    		"start:docker:debug": "node --inspect=0.0.0.0:9229 src/server.js"
    	},
    	"dependencies": {
    		"express": "^4.16.1"
    	}
    }
    

    The Docker configuration

    Following our guideline, we do not modify the initial docker-compose.yml but create a second one that extends the first one. We will use the [f flag](https://docs.docker.com/compose/reference/#use–f-to-specify-name-and-path-of-one-or-more-compose-files) of the docker-compose CLI to use them both.

    docker-compose.debug.yml

    services:
    	webapp:
    		command: [ 'npm', 'run', 'start:docker:debug' ]
    		ports:
    			- "127.0.0.1:8080:8080"
    			- "127.0.0.1:9229:9229"
    

    Then, to restart the service with debug mode enabled, you can use this command:

    docker-compose build
    docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
    

    The service is now ready to be attached to the VSCode debugger.

    Running the debugger with VSCode

    At the root of your project, create a new directory .vscode and add the following configuration file.

    .vscode/launch.json

    {
    	"version": "0.2.0",
    	"configurations": [
    		{
    			"type": "node",
    			"request": "attach",
    			"name": "Debug webapp",
    			"remoteRoot": "/app/src",
    			"localRoot": "${workspaceFolder}/webapp/src"
    		}
    	]
    }
    

    When adding a breakpoint, the remoteRoot and localRoot properties will match the file’s position in the VSCode environment and its location in the Docker service file system.

    You can now start the debugger on the webapp service. Open the debugging panel and select the Debug webapp option. Then click on the play button.

    The debugger is started.

    Add a breakpoint on line 6 and then go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar.

    The debugger stops on line 6 and we can see that the variable firstName is undefined. The problem comes from line 5 where this is a typo on the firstName parameter name.

    To close the debugger, click on the button with a red square.

    Debugging Multiple Docker Services

    The Node.JS micro-service

    To take this a step further, we will add another service, named micro-service, which will be called by webapp.

    First, copy and paste the contents of the webapp directory into another directory named micro-service.

    Then, in the webapp directory, install axios and update the code as follows.

    npm install axios
    

    webapp/src/server.js

    const express = require('express');
    const axios = require('axios');
    
    const app = express();
    
    app.get('/fullname', async (req, res, next) => {
    	try {
    		const { data: fullName } = await axios.get('<http://micro-service:8080/fullname>', {
    			params: req.query
    		});
    		res.send(fullName);
    	} catch (err) {
    		next(err);
    	}
    });
    
    app.listen(8080, () => console.log('Listening on port 8080...'));
    

    The URL used line 8 is based on the name of the Docker service defined in the next section.

    The Docker configuration

    Add the new service to your docker-compose.yml. Note that it uses a different port so as not to conflict with the webapp service.

    docker-compose.yml

    services:
    	webapp:
    		build: ./webapp
    		ports:
    			- "127.0.0.1:8080:8080"
    	micro-service:
    		build: ./micro-service
    		ports:
    			- "127.0.0.1:3001:8080"
    

    Then, in your docker-compose.debug.yml, add the new service as well. Note that the debugger port is also different from the first one.

    docker-compose.debug.yml

    services:
    	webapp:
    		command: [ 'npm', 'run', 'start:docker:debug' ]
    		ports:
    			- "127.0.0.1:8080:8080"
    			- "127.0.0.1:9229:9229"
    	micro-service:
    		command: [ 'npm', 'run', 'start:docker:debug' ]
    		ports:
    			- "127.0.0.1:3001:8080"
    			- "127.0.0.1:9230:9229"
    

    Now build and start the two services.

    docker-compose build
    docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
    

    Running multiple debuggers with VSCode

    The last thing to do is to add the configuration of the second debugger in launch.json.

    .vscode/launch.json

    {
    	"version": "0.2.0",
    	"configurations": [
    		{
    			"type": "node",
    			"request": "attach",
    			"name": "Debug webapp",
    			"remoteRoot": "/app/src",
    			"localRoot": "${workspaceFolder}/webapp/src"
    		},
    		{
    			"type": "node",
    			"request": "attach",
    			"name": "Debug micro-service",
    			"port": 9230,
    			"remoteRoot": "/app/src",
    			"localRoot": "${workspaceFolder}/micro-service/src"
    		}
    	]
    }
    

    Once the configuration is added, you can run the two debuggers for each service.

    Once both debuggers are started, add a breakpoint in each service and go to http://localhost:8080/fullname?firstName=Foo&lastName=Bar. The application will stop successively on each breakpoint.

    Your VSCode debugger is now fully configured to work with your Docker services. Congratulations!

  • Tail call: Optimizing recursion

    Recursive functions are recommended to solve some problems, or imposed by languages like pure functional programming languages which don’t have for and while loops. But recursion may comes at a price.

    Let’s use recursion to sum the elements of an array and understand how to optimize it with tail call optimization.

    Note that I use JavaScript and although tail call optimization appeared in ES 6 specifications, most engines don’t implement it, including V8. Apple WebKit is one of the only ones where it is available. Other languages implementing tail call optimization are listed in the Wikipedia article about tail call.

    Naive implementation

    function sum(x) {
    	// Base case. The sum of no elements is 0
    	if (x.length === 0) {
    		return 0;
    	}
    
    	// Add the first element to the sum of all the other elements
    	return x[0] + sum(x.slice(1));
    }
    

    It runs like this:

    Comes the call stack. The call stack keeps track of where the interpreter is in a program that calls multiple functions. When the program calls a function, the interpreter adds that function on the top of the call stack. When the function is finished, the interpreter takes it off the stack.

    Basically that’s how it computes sum([1, 2, 3]):

    [] is the base case. It returns 0. The interpreter has stacked 4 frames. Now it can start unstacking. With bigger arrays (about 10 000 elements depending on configuration), the call stack could reach its maximum size, crashing the program.

    Tail call to the rescue

    Tail call means that the final action of a function is a function call.

    In the previous implementation, the final action of the function was an addition.

    Let’s apply tail call. To do that, you have to add an accumulator argument to the function to save the partial sum. Using an accumulator is often required to apply tail call.

    function sum(x, acc = 0) {
    	// Base case: no more elements, returns the accumulator
    	if (x.length === 0) {
    		return acc;
    	}
    
    	// Call sum() without the first element
    	// Add the first element to the accumulator
    	// The recursive call to sum() is now the final action of the function
    	return sum(x.slice(1), acc + x[0]);
    }
    

    It runs like this:

    The interpreter doesn’t have to go back to the previous sum() call. It only has to go back to the original caller sumCaller(). Thereby, the call stack doesn’t need to keep track of all calls to sum(). That allows an optimization called tail call optimization. Interpreters that implement tail call optimization will only use one frame to keep track of the current call to sum().

    With tail call optimization, the call stack is like this:

    Now, even with bigger arrays, the call stack will only use one frame to compute sum().

    Be careful using recursion and think about tail call optimization.

  • Accessibility

    Accessibility

    Foreword

    This is no techie article but it does indeed talk about tech.

    Don’t expect code blocks below, but I trust I’ll broach some important thinking points. I’m also no definitive expert on the subject, but I have always found the discussed issue to be dramatically important in our ecosystem.

    So please bear with me!

    Let’s play a game

    Imagine if you will that you’re surfing merrily on your favorite handheld device on a warm autumn afternoon. Battery chock-full of energy, 4G (5G even?) pouring kitten photos on your nice 90Hz OLED screen, fluid interface anticipating your every needs.

    And all is well.

    Now. Now, imagine that the sun suddenly shines through the low-hanging clouds and glares over your screen.

    You’re handicapped.

    Imagine your ISP decided you did not give them enough kitten-money and throttles your connection.

    You’re handicapped.

    Imagine it’s now winter and real darn cold. You’re wearing gloves, and the touch-screen doesn’t register your fingers.

    You’re handicapped.

    You get a notification. But your phone’s vibrator is deactivated. Lucky for you, it does ring. Unlucky for you, you work in a loud factory.

    You’re handicapped.

    BUT WAIT!

    I’m no handicapped person I hear you cry out, slightly worried.

    Well, you yourself might not be disabled, but you definitely will be handicapped from time to time, maybe less than others, maybe more, definitely for different reasons.

    What of it?

    Accessibility isn’t a buzzword or a fad. Accessibility isn’t just another metric or a Lighthouse report entry. Accessibility is actually one of the founding principles of the Internet (and maybe the most important one).

    <aside> ℹ️ Don’t quote me on that. !! You might want to quote the Contract for the web’s principles 1 though.

    </aside>

    (huge) list of 584 companies supporting Contract for the web

    All those guys and girls must sure know what they’re talking about.

    And what do we find there first? Right at the top?

    Ensure everyone can connect to the internet

    And just a bit further?

    Make the internet affordable and accessible to everyone

    Although these two principles are aimed respectively at governments and companies, I do believe that everyone is part of the Web (World-Wide, remember?) and should act accordingly. And it has nothing to do whatsoever with our personal mental and / or physical abilities.

    Accessibility is not about handicap. It is not about disability. It is no luxury. It’s the basis of an ever-expanding global network that cannot be allowed to leave anyone behind. It might sound cheesy, overly philosophical or even utopian, but it has direct implications for anyone building (for) the Web and potentially dire repercussions for anyone using it.

    1. Net neutrality is no joke
    2. No-one is immune to being handicapped
    3. Accessibility is a duty of those building tools online
    4. Accessibility is the standard and not the other way around
    5. Building accessible platforms means building better platforms tech-wise
    6. It’s no ethical conundrum, but it is an ethical imperative

    Let us dive deeper then.

    1. Net neutrality

    This one might surprise you, as it’s not commonly associated with handicap, but it might just be as important for this topic. Indeed, as we thrive not to discriminate handicapped users, we also must fight against data discrimination.

    If you’re throttled by your ISP, you’re not going to be able to use the Web normally.

    Let’s never give up on this one. It affects every single person as independent decentralized internet access is still somewhat of a utopia.

    I won’t go into too much details here, as so many much more relevant and informed people are fighting everyday for it.

    Do go support them though!

    2. Everyone is liable to handicap (yes even you)

    Disability and handicap are two very different concepts. If a disabled person might be handicapped most of the time, a perfectly able person will be handicapped from time to time. As evolved as we may like to think our society is, able-ism is still very present on- and off-line. And it does not stop at too few wheelchair-accessible stores and homes.

    It does not stop at allowing visually impaired people to use your website either. In this wonderful diversity of humans all around the disc globe, everyone has different needs and different ways of consuming the Web. Be it by aid of a screen-reader, by zooming every webpage to 200% or by using a slow reconditioned device inherited from your third sibling. You just can NOT assume everyone is on a broadband connection enabled desktop with a fullHD screen in good lighting conditions.

    An example so dumb it might seem trivial is the difference between touch-devices and mouse usage. You just don’t click the way you tap. And the other way round. And you must take both use-cases into account. Especially since the borders between devices seems to be fading (looking at you convertible laptops and stylus phones).

    3. As a developer it’s your job to build accessible platforms, not the user’s

    You might rarely see a web developer job offer containing explicit mention of accessibility, but it’s not because it’s not part a of the tasks. It’s because it’s an implicit prerequisite. As a developer, it is your duty to take it into account.

    If you love the Web (and I hope you do if you build it) you’re expected to take its philosophy into account. It’s right there in the name : World-Wide Web. World-Wide. Entire populations. The entire population.

    4. Accessibility is no afterthought. It’s the basis.

    It’s often thought that you can build stuff and make it accessible afterwards. That if you find some time at the end of the sprint you might either add aria attributes or enjoy a day off.

    Well… NO. If you don’t include it from the ground up, you are discriminating users. And discrimination is bad, m’kay? It’s no quick-win to block people from using your app.

    5. Virtuous circles and best practices

    Think about it. If you build stuff right on one aspect, chances are other parts of the system will benefit from it.

    Accessibility as a standard helps you build better tech.

    Taking time to think about the tabbing index of your page to allow users to navigate it using their keyboard can only improve the zoning and hierarchy of the page.

    Taking time to improve the contrast ratios of your UI will improve it’s design.

    Taking time to label your actionable elements will also improve the page’s SEO.

    Lowering your data payloads for low-bandwidth users will speed up page load for everyone.

    It’s win-win on so many levels.

    6. Ethics bro

    Ethics has always had bad press if any press at all. The trolley problem might spring to mind. Or NBC’s “The good place”. Or even the bible, perhaps?

    All things whatsoever ye would that men should do to you, do ye even so to them.

    Erm… English please?

    Do unto others as you would have them do unto you.

    That’s about it. As a Web-builder, you should create software that treats people ethically. Period. No maybe, no buts, no ifs, you just cannot expect people to use your software in ideal conditions and according to your own plan.

    You cannot predict the every use case, nor should you, you can just embrace accessibility as one of the basic building blocks of your app to ensure no one is left behind.

    Yes, even you.

    Footnotes

    [1] Principles for the web

    • “Ensure everyone can connect to the internet”.
    • “Keep all of the internet available, all of the time”.
    • “Respect and protect people’s fundamental online privacy and data rights”.
    • “Make the internet affordable and accessible to everyone”.
    • “Respect and protect people’s privacy and personal data to build online trust”.
    • “Develop technologies that support the best in humanity and challenge the worst”.
    • “Be creators and collaborators on the Web”.
    • “Build strong communities that respect civil discourse and human dignity”.
    • “Fight for the Web”.
  • Comment bétonner l’intégration d’une API

    Quand on développe une application d’une certaine taille, on fait généralement appel à des solutions tierces pour certaines fonctionnalités qui ne sont pas “coeur de métier”, ou tout simplement pour des besoins annexes à l’application (CRM et compagnie).

    Et si tout va bien, cette solution va nous fournir des API pour échanger avec elle.

    Parfois ces API fonctionnent parfaitement, et tout le monde est content. Spoiler alert : c’est loin d’être toujours le cas !

    Le but n’étant de taper sur personne, nous allons imaginer une API fictive, qui fonctionnera plus ou moins bien pour démontrer ce qui m’arrange.

    Voilà ce qui va se passer :

    On dispose d’une liste de villes pour lesquelles on a besoin de récupérer la météo.

    On va aller de l’implémentation la plus simple à la plus robuste.

    Si vous maîtrisez JavaScript, vous devriez connaître au moins jusqu’au niveau 2.

    Mais j’ai bon espoir de vous faire découvrir des outils sympa dans la suite, restez jusqu’au bout (le 7ème va vous étonner).

    Si vous voulez tester au fil de l’eau, tous les exemples sont disponibles ici : https://replit.com/@rsauget/blog-api

    Niveau 0 – naïf

    import _ from 'lodash';
    
    async function getWeather({ city }) {
    	// On ajoute un peu de délai pour se rendre compte de l'impact
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
    }
    
    const cities = ['Lyon', 'Montpellier', 'Paris'];
    
    const results = {};
    
    for (const city of cities) {
      results[city] = await getWeather({ city });
    }
    
    console.log(results);
    // {
    //   Lyon: 'Soleil',
    //   Montpellier: 'Soleil',
    //   Paris: 'Pluie',
    // }
    

    On a ici une première version très simple : on boucle sur les villes demandées, et pour chacune on va récupérer le temps qu’il y fait. On agrège les résultats dans un objet.

    Dans un monde parfait, cette solution pourrait convenir.

    En pratique, vous aurez surement noté plein de défauts : c’est bien le but, et d’ici quelques minutes j’espère que nous en aurons traité la majorité !

    Le premier défaut de cette version naïve est d’effectuer une requête après l’autre, de façon séquentielle, comme si on avait besoin de la météo de la première ville avant de demander la météo de la seconde. On sent qu’il y a mieux à faire.

    Version 1 – exécution “parallèle”

    Le but n’étant pas de rentrer dans le détail de la gestion de l’asynchronicité en Javascript, vous me pardonnerez le raccourci de parler d’exécution parallèle.

    On va donc utiliser un Promise.all pour paralléliser les appels à notre service externe :

    import _ from 'lodash';
    
    async function getWeather({ city }) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
    }
    
    const cities = ['Lyon', 'Montpellier', 'Paris'];
    
    // On construit l'objet résultant à partir de paires [clé, valeur]
    const results = Object.fromEntries(
      await Promise.all(
        cities.map(
          async (city) => [city, await getWeather({ city })],
        ),
      ),
    );
    
    console.log(results);
    // {
    //   Lyon: 'Soleil',
    //   Montpellier: 'Soleil',
    //   Paris: 'Pluie',
    // }
    

    Ca fonctionne bien parce qu’on a trois villes, et que notre API peut gérer 3 requêtes concurrentes les doigts dans le nez.

    Essayez d’en mettre 1000 pour voir : il y a de bonnes chances que l’API vous dise gentiment de vous calmer.

    Dans le meilleur des cas, ce sera une réponse HTTP cordiale : 429 - Too Many Requests.

    Mais ça peut passer par un timeout, une déconnexion impromptue (bonjour ECONNRESET), une erreur 500, ou même vous faire bloquer votre IP. Pas génial.

    Pour pallier ce problème, on va limiter le nombre de requêtes concurrentes.

    Version 2 – parallèle, mais pas trop

    On pourrait traiter les requêtes par paquets de 10 par exemple, mais l’idéal serait une fenêtre glissante : on en lance 10, et à chaque fois qu’une termine on en relance une, de sorte qu’on en ait toujours 10 qui tournent.

    Il y a une petite bibliothèque qui permet précisément de faire ça : p-limit (https://www.npmjs.com/package/p-limit)

    import _ from 'lodash';
    import pLimit from 'p-limit';
    
    async function getWeather({ city }) {
      await new Promise((resolve) => setTimeout(resolve, 10));
    	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
    }
    
    const cities = _.range(1000).map(n => `Ville ${n}`);
    
    // Vous pouvez jouer avec la limite pour voir l'impact sur le temps d'exécution
    const limit = pLimit(10);
    
    // On va wrapper l'appel avec p-limit, qui gèrera quand déclencher la fonction
    const limitedGetWeather = (city) => limit(
      async () => [city, await getWeather({ city })],
    );
    
    const results = Object.fromEntries(
      await Promise.all(
        cities.map(limitedGetWeather),
      ),
    );
    
    console.log(results);
    

    Il y a d’ailleurs toute une famille de bibliothèques autour de p-limit : p-all, p-map

    Je vous invite à y jeter un oeil, elles sont bien pratiques et assez légères.

    En jouant sur le nombre de requêtes concurrentes qu’on autorise, on peut arriver à coller à peu près à ce que tolère l’API.

    Mais on peut faire mieux.

    Version 3 – parallèle juste ce qu’il faut

    La plupart des API explicitent leur politique de rate-limit dans leur documentation. Elle est généralement exprimée en nombre de requêtes par unité de temps, par exemple 10000 requêtes par minute.

    Et bien là encore, une bibliothèque qui gère ça très bien : bottleneck (https://www.npmjs.com/package/bottleneck)

    Contrairement à p-limit, bottleneck est assez costaude, mais gère tout un tas de situations :

    • “réservoirs” (on vous redonne 100 requêtes toutes les 10 secondes par exemple)
    • groupes : un ensemble de limiteurs similaires identifiés par une clé
    • distribué : on peut le connecter à un redis et le limiteur est synchronisé entre plusieurs services
    • politique de “débordement” : si on fait beaucoup plus de requêtes que la limite autorisée, les appels vont s’accumuler dans la file d’attente ; on peut décider de limiter la taille de cette file et de définir ce qu’on fait quand ça “déborde”
    • etc.

    On peut ainsi traiter à peu près tous les cas de figure. Mais pour aujourd’hui, restons sur l’exemple simple de 10000 requêtes par minute.

    import _ from 'lodash';
    import Bottleneck from 'bottleneck';
    
    // Vous pouvez jouer avec les limites pour voir l'impact sur le temps d'exécution
    const limiter = new Bottleneck({
      // 10000 requêtes par minute => 6ms entre chaque requête
      minTime: 6,
      // c'est généralement une bonne idée d'aussi limiter la concurrence
      maxConcurrent: 5,
    });
    
    // On wrap l'appel à l'API avec bottleneck, pour en limiter le flux
    const getWeather = limiter.wrap(async ({ city }) => {
      await new Promise((resolve) => setTimeout(resolve, 10));
    	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
    });
    
    const cities = _.range(1000).map(n => `Ville ${n}`);
    
    const results = Object.fromEntries(
      await Promise.all(
        cities.map(
          async (city) => [city, await getWeather({ city })],
        ),
      ),
    );
    
    console.log(results);
    

    Grâce à bottleneck on peut être au plus proche du rate limit de l’API, et ainsi aller le plus vite possible sans se faire flasher.

    Version 4 – gestion d’erreur

    Je crois que c’est La Fontaine qui disait que rien ne sert de courir si c’est pour se gauffrer sur la première erreur venue (https://fr.wikipedia.org/wiki/Le_Lièvre_et_la_Tortue_(La_Fontaine)).

    Et bien c’est exactement ce qu’on est en train de faire. Si une seule requête échoue, elle entraîne toutes les autres dans sa chute, et on se retrouve sans aucun résultat.

    Ça peut être souhaitable vous me direz. Mais supposons pour l’exemple qu’on préfère savoir le temps qu’il fait dans 99% des villes plutôt que dans aucune.

    En plus, le Promise.all a un comportement qui peut surprendre : toutes nos requêtes vont s’exécuter, quoi qu’il arrive. Certaines peuvent faire des erreurs, ça n’arrête pas les suivantes pour autant. Et cerise sur le gâteau, l’erreur qui va remonter au final sera seulement la première de la liste.

    En résumé, en cas d’erreur il est difficile de savoir exactement ce qui s’est passé et ce qui a fonctionné ou pas. Ça va qu’on ne fait que des GET, imaginez si on était en train de passer des transactions bancaires…

    On peut mettre un try/catch dans notre fonction getWeather, et traiter chaque erreur individuellement. Ça laisse la possibilité de re-throw l’erreur en cas de panique.

    Mais si on souhaite gérer les erreurs dans leur ensemble, on a une autre fonction : Promise.allSettled(disponible à partir de node 12.10.0 https://node.green/#ES2020-features–Promise-allSettled).

    import _ from 'lodash';
    import Bottleneck from 'bottleneck';
    
    const limiter = new Bottleneck({
      minTime: 6,
      maxConcurrent: 5,
    });
    
    const getWeather = limiter.wrap(async ({ city }) => {
      await new Promise((resolve) => setTimeout(resolve, 10));
      // On a 1 chance / 10 d'avoir une erreur
      if (_.random(10) === 0) throw new Error('ECONNRESET');
    	return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
    });
    
    const cities = _.range(1000).map(n => `Ville ${n}`);
    
    // On zip les villes avec le résultat des requêtes pour faire des paires [ville, resultat]
    const results = _.zip(
      cities,
      await Promise.allSettled(
        cities.map(
          (city) => getWeather({ city }),
        ),
      ),
    );
    
    // Chaque résultat de Promise.allSettled est de la forme : { status: 'fulfilled', value: 'Résultat' }
    // ou { status: 'rejected', reason: 'Erreur' }
    // Ce qui avec le zip donne un results de la forme :
    // [
    //   ['Ville 1', { status: 'fulfilled', value: 'Soleil' }],
    //   ['Ville 2', { status: 'fulfilled', value: 'Pluie' }],
    //   ['Ville 3', { status: 'rejected', reason: 'ECONNRESET' }],
    //   ['Ville 4', { status: 'fulfilled', value: 'Pluie' }],
    // ]
    // _.partition va nous séparer la liste des résultats en deux listes, selon que la requête a réussi ou non
    const [successes, failures] = _.partition(results, ['1.status', 'fulfilled']);
    
    if (!_.isEmpty(failures)) {
      console.error(
        _.chain(failures)
          .groupBy('1.reason')
          .mapValues((results) => _.map(results, _.first))
          .value(),
      );
    }
    
    console.log(
      _.chain(successes)
        .fromPairs()
        .mapValues('value')
        .value(),
    );
    

    Promise.allSettled nous retourne la description du résultat de l’ensemble de nos requêtes, qu’elles aient réussi ou pas. Libre à nous de gérer les succès et erreurs comme on le souhaite.

    Maintenant, toutes les erreurs ne se valent pas.

    Si on demande une ville qui n’existe pas par exemple, on peut se contenter de retourner une météo undefined.

    Mais si le service est juste momentanément indisponible, on peut vouloir essayer une nouvelle fois.

    Version 5 – on persévère

    Un des grands préceptes Shadok nous enseigne que plus ça rate, plus on a de chance que ça marche (https://fr.wikipedia.org/wiki/Les_Shadoks).

    Dans notre cas, si l’API nous renvoie une erreur momentanée, ou indéterminée, on va vouloir retenter notre chance, jusqu’à ce que ça retombe en marche.

    Vous me voyez venir ?

    Bien vu, j’ai une bibliothèque pour ça : async-retry (https://www.npmjs.com/package/async-retry)

    Elle permet de réessayer une requête en erreur, avec de chouettes options :

    • délai entre les tentatives, avec un facteur si on veut l’augmenter de manière exponentielle, et également un facteur aléatoire si on veut
    • limite en nombre de tentatives et/ou en temps
    • annuler les tentatives restantes, en cas d’erreur fatale par exemple

    On peut ainsi faire quelque chose comme :

    import _ from 'lodash';
    import Bottleneck from 'bottleneck';
    import retry from 'async-retry';
    
    const limiter = new Bottleneck({
      minTime: 6,
      maxConcurrent: 5,
    });
    
    const getWeather = limiter.wrap(
      async () => {
        await new Promise((resolve) => setTimeout(resolve, 10));
        if (_.random(10) === 0) throw new Error('ECONNRESET');
        return _.sample(['Soleil', 'Nuages', 'Pluie', 'Neige']);
      },
    );
    
    // On wrap l'appel avec async-retry, pour la gestion d'erreur
    const getWeatherRetry = async ({ city }) => retry(
      async () => getWeather({ city }),
      {
        // On retente toutes les 100ms dans la limite de 5 fois
        retries: 5,
        minTimeout: 100,
        factor: 1,
      }
    );
    
    const cities = _.range(1000).map(n => `Ville ${n}`);
    
    const results = _.zip(
      cities,
      await Promise.allSettled(
        cities.map(
          (city) => getWeatherRetry({ city }),
        ),
      ),
    );
    
    const [successes, failures] = _.partition(results, ['1.status', 'fulfilled']);
    
    if (!_.isEmpty(failures)) {
      console.error(
        _.chain(failures)
          .groupBy('1.reason')
          .mapValues((results) => _.map(results, _.first))
          .value(),
      );
    }
    
    console.log(
      _.chain(successes)
        .fromPairs()
        .mapValues('value')
        .value(),
    );
    

    Attention toutefois : c’est sympa de réessayer les requêtes jusqu’à ce que ça marche, comme un bourrin. Quand on GET de la donnée comme ici, il ne peut rien se passer de trop gênant. Si la requête fait des modifications en revanche, ça n’est pas toujours une bonne idée.

    Prenons un exemple au hasard : imaginez que Netflix fasse ça sur la requête de paiement de votre abonnement, et que celle-ci soit rejouée 10 fois. Eh ben ça va pas faire une soirée très “chill”, comme disent les jeunes.

    Il faut donc toujours se poser la question de l’impact de faire la requête plusieurs fois sur le fonctionnement du système, avant de mettre en place une telle logique.

    C’est aussi sympa pour l’API de mettre un délai exponentiel. Si elle est sous l’eau, vous lui laisserez ainsi une chance de reprendre ses esprits avant de vous répondre.

    Conclusion

    En combinant une logique de rate-limit avec une bonne gestion d’erreur, comme on a pu le faire avec le combo gagnant bottleneck + async-retry, on arrive à un code relativement tout-terrain.

    On l’utilise chez Indy pour certaines synchronisations un peu capricieuses, et ça tourne comme une horloge.

    Et pour finir sur une note plus sérieuse, gardez à l’esprit qu’il y a aussi des dev derrière les API sur lesquelles on aime râler, et que c’est très difficile, voire impossible, de fournir un service parfaitement fiable. Soyez compréhensifs, et raisonnables dans vos intégrations, ne faites pas les bourrins 😉

  • Récupérer les coordonnées dans un fichier pdf avec pdfjs-dist

    Contexte

    Chez Indy on fait de la compta :p. Nous devons donc produire différents documents (en pdf) qui seront soit télétransmis soit utilisés par nos clients afin de remplir les mêmes documents sur notre joli site des impôts.

    Presque chaque année, les documents changent (dans notre jargon on parle de millésime) et on doit mettre à jour notre générateur de document. Les documents sont mis à disposition par l’État et sont des PDF sans zone de texte clairement définie.

    Du coup nous prenons le pdf comme une image de fond et nous écrivons par dessus les bonnes données de chaque client. Il nous faut donc les bonnes coordonnées dans le pdf.

    Nous avons plusieurs problématiques.

    Différence entre PDFs

    La première difficulté est de voir la différence entre 2 millésimes. Nous utilisons pour ça l’outil diff-pdf (https://vslavik.github.io/diff-pdf/).

    Pour l’installer sur mac c’est facile (pour les autres OS je vous laisse regarder ici : https://github.com/vslavik/diff-pdf#obtaining-the-binaries) :

    brew install diff-pdf
    

    Ensuite on peut facilement voir la différence entre 2 pdf avec la commande suivante :

     diff-pdf --view 2033-sd_3330.pdf 2033-sd_3723.pdf
    

    La commande ouvrira une visionneuse (par contre c’est moche 😛).

    Récupérer les coordonnées dans un pdf

    Afin d’avoir les coordonnées, nous pouvons utiliser facilement des outils web. Pour les curieux, le lien vers le code final se trouve en bas de l’article.

    Nous commençons par créer le fichier html. Nous avons besoin d’un canvas. Nous allons aussi créer un span où on mettra les coordonnées.

    <div id="app">
      <div class="coord">Current: <span id="coord"></span></div>
      <canvas id="canvas"></canvas>
    </div>
    

    Un peu de css pour bien voir les positions.

    .coord {
      background-color: black;
      color: cornsilk;
      width: 135px;
      position: fixed;
    }
    

    Puis nous allons créer le fichier index.ts

    import { pdf } from './pdf';
    import * as pdfjsLib from 'pdfjs-dist';
    import pdfjsWorkerEntry from 'pdfjs-dist/build/pdf.worker.entry.js';
    pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerEntry;
    
    const canvas = document.getElementById('canvas') as HTMLCanvasElement;
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    
    const load = async () => {
      // le pdf est en base64 car le site des impôts n'accepte pas les requêtes cors
      const pdfDoc = await pdfjsLib.getDocument(pdf).promise;
      const page = await pdfDoc.getPage(1);
      const viewport = page.getViewport({ scale: 1.5 });
      const renderContext = {
        canvasContext: ctx,
        viewport: viewport,
      };
      canvas.height = renderContext.viewport.height;
      canvas.width = renderContext.viewport.width;
      await page.render(renderContext).promise;
    
      function getMousePos(e: MouseEvent) {
        var rect = canvas.getBoundingClientRect();
        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
      }
    
      canvas.addEventListener(
        'click',
        function (e) {
          const pos = getMousePos(e);
    
          /// check x and y against the grid
          const [x, y] = viewport.convertToPdfPoint(pos.x, pos.y) as any;
          const currPos = [parseInt(x, 10), parseInt(y, 10)];
    
          document.getElementById(
            'coord'
          )!.textContent = `${currPos[0]}, ${currPos[1]}`;
        },
        false
      );
    };
    
    load();
    

    La ligne suivante permet de faire fonctionner correctement pdfjs-dist avec un worker.

    import pdfjsWorkerEntry from 'pdfjs-dist/build/pdf.worker.entry.js';
    pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerEntry;
    

    Le point important du code est à la ligne 33 avec viewport.convertToPdfPoint. Cette méthode permet d’avoir les bonnes coordonnées à l’intérieur du pdf. Elle va convertir le x, y du navigateur en coordonnées du pdf.

    Une fois que vous aurez cliqué sur une zone du pdf, vous allez voir les coordonnées dans le bloc noir en haut à gauche.

    Vous pouvez voir le résultat ici : https://typescript-qgcwq5.stackblitz.io

    Et voir le code là : https://stackblitz.com/edit/typescript-qgcwq5

    ❗ Le fichier pdf.ts est là car :

    • stackblitz (free) ne permet pas de déposer un fichier
    • impossible de récupérer les fichiers pdf via fetch directement par le site des impôts (ils bloquent les requêtes cors)
  • Les Caribous d’Indy 🦌

    Dans une startup on grandit vite, on sort rapidement de nouvelles fonctionnalités et de nouveaux projets. Les équipes tech & produit passent la majorité de leur temps sur ces nouveaux sujets pour toujours répondre au mieux aux besoins des clients.

    Il ne faut cependant pas négliger la maintenance de l’existant pour bâtir de solides fondations. Plus les fonctionnalités et projets s’accumulent, et plus le temps à accorder à la maintenance devient important. C’est pourquoi chez Indy nous avons mis en place ce que l’on appelle les Caribous 🦌.

    Le Caribou qu’est-ce que c’est ?

    Le Caribou est la personne en charge de la maintenance. C’est elle qui, en priorité, doit se charger de :

    • répondre aux tickets sur le support interne
    • débloquer ses collègues (au support notamment) en cas de problèmes
    • surveiller les alertes remontées par notre monitoring (ex: issues Sentry*) et alerter les équipes en cas de gros pépin
    • vérifier et régler les incohérences de données trouvées par nos algorithmes d’audit
    • s’assurer de la bonne santé de nos tâches asynchrones (ex: envoi des liasses à l’administration fiscale)

    Quel intérêt ?

    L’intérêt de dédier une personne à ces tâches est notamment de soulager les autres devs de l’équipe. En effet, avant que nous ne mettions en place ce système de Caribou, chaque dev avait l’obligation d’activer ses notifications slack sur nos différents channels d’alerte, et donc à tout moment pouvait être interrompu dans son travail. Et vous savez, comme moi, à quel point cela peut être difficile de changer de contexte régulièrement !

    Aujourd’hui, quand nous ne sommes pas Caribou, nous pouvons nous concentrer pleinement sur les tâches en cours, tout en sachant que les problèmes seront bien traités ! Bien sûr il n’est pas non plus interdit de donner un coup de main au caribou entre deux tâches 😉 !

    Chez Indy, et plus particulièrement dans mon équipe ScaleOps, le Caribou a été un véritable tournant dans l’organisation de l’équipe. Cela a permis à notre PM de se décharger d’une grosse part du temps qu’il passait à répondre aux tickets. Cela a aussi permis à tous les devs de l’équipe de se sentir investi équitablement dans les tâches de maintenance et de soutien aux autres équipes.

    Comment ça fonctionne ?

    Chez Indy, libre à chaque équipe de mettre en place ou non le système de Caribou et de la manière qu’elle le souhaite.

    Chez ScaleOps, nous avons établi un planning sur l’année pour attribuer le rôle de Caribou à tour de rôle chaque semaine à un dev différent. En cas de vacances ou d’absence, on s’organise dans l’équipe pour modifier l’ordre d’attribution.

    Le Caribou occupe le rôle pour une semaine entière, ce qui permet aux autres de vraiment se concentrer sur leurs tâches de développement, et en même temps ne dure pas trop longtemps, pour éviter au Caribou de se lasser des tâches de maintenance et de trop couper avec les sujets de roadmap en cours.

    Organisation du Caribou chez ScaleOps

    Dans mon équipe, le Caribou s’organise de la manière qu’il le souhaite. Nous avons cependant un ordre de priorité à accorder aux différents sujets.

    En effet, nous cherchons en priorité à régler les alertes remontées sur Sentry, car elles peuvent potentiellement impacter plusieurs utilisateurs (voire tous). Ensuite, nous nous attelons à traiter les tickets internes, qui concernent généralement un seul utilisateur.

    Pour ce qui est des audits (jobs qui tournent en tâche de fond afin de vérifier la cohérence des données de nos utilisateurs) nous regardons qu’ils tournent correctement qu’une fois par semaine, le volume d’erreurs étant faible nous n’avons pas besoin de plus.

    Après avoir vérifié tout ça, s’il reste du temps dans la journée du Caribou, celui-ci peut attaquer le développement des correctifs pour régler à la racine des problèmes remontés par les alertes Sentry, ou bien s’attaquer à de petits sujets de roadmap !

    Points d’attention

    Comme nous l’avons vu, le rôle du Caribou est d’être la vigie de l’équipe, et donc d’intervenir en premier en cas de problème. Par conséquent, s’il ne connaît pas assez le domaine concernant un de ces problèmes, il se doit de prévenir et de voir avec la personne compétente la meilleure manière d’aborder la situation.

    Notons aussi qu’il faut garder une bonne communication avec son PM, notamment lorsqu’il s’agit de problèmes nécessitant un développement plus conséquent. En effet, tous les problèmes ne sont pas à traiter dans l’instant. Il arrive que certains demandent plus de réflexion sur la manière de les solutionner, et donc il faudra sûrement intégrer ça dans la roadmap avec le PM.

    Un point très positif de ce système est qu’il permet de partager la connaissance facilement. Le Caribou aura à cœur de documenter un maximum les actions qu’il prend face à un problème (ex: méthode d’investigation et de résolution, différentes requêtes utilisées, etc.) dans ce que nous appelons la Caribase. Cela permettra aux prochains d’être plus efficaces face à un même problème. De plus, lorsque nous accueillons de nouveaux collègues, assister le Caribou est une excellente manière d’aborder la plupart des sujets de l’équipe !

    Et vous, comment fonctionnez-vous ? Que pensez-vous de notre système de Caribou 🦌 ?

  • Créer un index B-tree composé efficace,

    Créer un index B-tree composé efficace, un défi plein de pièges

    Nous avons tous eu à indexer nos bases de données à un moment donné.

    Nous avons tous cherché à indexer plusieurs champs avec des index composés à un moment donné.

    Et pourtant nos index composés peuvent avoir fait pire que mieux ou ne pas être à la hauteur. Nous allons ici avec un véritable use case voir la différence entre les bons et les mauvais index composés.

    Rappel du B-tree et idées reçues

    Etant donné l’étendu de la littérature sur les B-tree, nous ne nous étendrons pas trop dessus. Pour mémoire, le fonctionnement du B-tree est le suivant

    Pour lire la valeur 7, on va traverser l’arbre en partant du nœud racine en bleu, traverser la branche jaune à gauche et lire les valeurs en vert. La, l’index va nous donner directement l’adresse de l’enregistrement voulu.

    Dans le cas d’un index composé, l’index sera une succession d’index simples.

    Exemple de schéma si on créé un index B-tree sur les colonnes “A” et “B” (dans cet ordre).

    Par exemple avec la requête SQL

    CREATE INDEX "composed_index_name" ON "my_table" ("A", "B");
    

    On va créer l’arbre suivant

    Si maintenant, je veux la valeur A: 1, B: 5 voici le parcours (en rouge) qui va être effectué.

    Si on suit le trajet en rouge lors d’une requête : aucun doute c’est vraiment bien optimisé.

    De plus, en observant l’arbre, on voit apparaître un pattern assez connu et souvent documenté.

    Un index sur A et B permet aussi d’indexer des queries sur A uniquement

    Si, je reprend, l’exemple, si je veux tous les enregistrements ayant pour valeur de A à 1 il suffit de parcourir l’arbre en rouge ci-dessous.

    Relativement peu de flèches rouges, ça semble être effectivement une bonne astuce à première vue.

    Le piège du composed index

    Nous allons maintenant nous intéresser à l’index composé et essayer de voir comment il peut devenir un piège.

    Les données théoriques de nos index

    Pour ce faire, nous allons nous intéresser à de la donnée plus proche de la réalité que notre exemple théorique. On va stocker en base de données des exécutions de tâches s’exécutant trois fois tous les dix jours et échouant une fois sur trois.

    Ce qui nous donne les données suivantes :

    datestatus
    2022-01-01success
    2022-01-05error
    2022-01-10success
    2022-01-11success
    2022-01-15error
    2022-01-20success
    2022-01-21success
    2022-01-25error
    2022-01-30success

    Si nous avons les deux requêtes suivantes à indexer :

    • Récupérer un run pour une date donnée
    • Un compte des runs en échec entre deux dates. L’exemple s’appliquerait aussi si nous voulons tous les runs en échecs (sans compte), mais la récupération des runs serait coûteuse en temps, le count nous permet de mesurer uniquement le temps de parcours de l’index en excluant le fetch de données.
    # Requête 1
    SELECT * from runs WHERE date = <date>
    
    #Requête 2
    SELECT count(*) from runs WHERE date > <date_start> AND date < <date_end> AND status = 'error'
    

    Premier index

    En bon développeur devant une requête SQL lente, nous allons penser index.

    A partir de l’idée reçue vue au paragraphe précédent, nous allons préférer l’index suivant :

    CREATE INDEX "date_status" ON "runs" ("date", "status");
    

    Cet index, semble couvrir les deux queries avec un seul index.

    Intéressons nous maintenant à l’arbre que cet index va générer.

    Par rapport à ce à quoi nous nous attendions, celui-ci semble étrange.

    En effet, nous n’avons qu’un enregistrement par bloc de feuilles (en vert). Et il y a beaucoup de flèches.

    Nous allons maintenant exécuter nos deux requêtes :

    Récupérer un run pour une date donnée

    Si je veux récupérer le run du 21/01/2022, je vais exécuter le trajet suivant (en rouge).

    Le trajet semble plutôt bien optimisé. Peu de flèches rouges empruntées. Vérifions ça par la pratique. Nous allons utiliser la base de données témoin contenant 10 millions de runs avec un run sur 100 en erreur.

    ScénarioTemps d’exécutionTaille d’index
    Sans index+/- 2s0Mb
    index date_status+/- 2ms160Mb
    index date+/- 2ms120Mb

    On remarque que le temps d’exécution est très proche avec l’index date_status qu’avec l’index date uniquement.

    Le contrat est donc clairement rempli avec cet index sur cette requête.

    Seul bémol, la taille de l’index est bien supérieure à celle de l’index date uniquement on comprend aisément pourquoi en revenant à l’arbre. La surabondance de blocs et de flèches se paye sur la taille de l’index.

    Tous les runs en échec entre deux dates

    Les choses vont se compliquer dans ce cas. Dans notre exemple théorique, si je demande une plage de date équivalente à la moitié de l’amplitude de la base, on se retrouve avec le parcours suivant.

    Pour finalement récupérer un seul enregistrement, nous avons un parcours extrêmement complexe et coûteux.

    Si on valide par la pratique, en reprenant notre jeu de données de test, on va demander une plage de date correspondant à environ 500 000 runs (sur 10 millions de runs dans la table).

    ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
    Sans index+/- 2s+/- 2s0Mb
    index date_status+/- 2ms+/- 310ms160Mb
    index date+/- 2ms+/- 300ms120Mb
    index status+/- 2s+/- 100ms45Mb

    On observe effectivement que notre index composé ne fait pas mieux qu’un index uniquement sur le champ date.

    Il est important de noter que plus la fenêtre temporelle de requête est large, pire vont être les performances de cet index.

    A l’inverse, plus la fenêtre est étroite meilleur sera l’index.

    C’est donc un véritable camouflet pour le postulat de départ : un index composé sur A-B permet de requêter sur A de la même façon que un index sur A

    On observe même une valeur étrange, la requête est plus rapide avec l’index status que date_status. Pourquoi ? Tout s’explique avec les cardinalités. Nous avons 10 millions de runs dont 1% en erreur. Soit 100 000 runs alors que dans le parcours via date_status nous allons au final **parcourir 500 000 runs.

    Ce qui nous amène a une première grande idée générale, un index composé efficace est le plus souvent un index réfléchi pour une et une seule requête.

    Autre première observation, un index composé occupe environ le même espace que les index uniques qui le compose. Ca semble logique si on repense à la structure de l’arbre.

    Comment exploiter au mieux le composed index

    A partir de là, nous allons réfléchir aux solutions d’indexation qui s’offrent à nous et essayer d’en tirer quelques règles.

    Indexer chaque champ

    Contre toute attente, cette solution, sans aucune subtilité est souvent une bonne approche.

    CREATE INDEX "date" ON "runs" ("date");
    CREATE INDEX "status" ON "runs" ("status");
    

    SI nous reprenons l’exemple précédent, voici le résultat du bench.

    ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
    Sans index+/- 2s+/- 2s0Mb
    index date_status+/- 2ms+/- 310ms160Mb
    index date+/- 2ms+/- 300ms120Mb
    index status+/- 2s+/- 100ms45Mb
    index date + index status+/- 2ms+/- 300ms160Mb

    C’est encore un coup d’épée dans l’eau, on ne fait pas mieux que avec date uniquement et la taille des index a augmenté.

    Inverser l’index composé

    Plutôt que d’avoir un index sur date puis status, on va inverser et le créer sur status puis date.

    CREATE INDEX "status_date" ON "runs" ("status", "date");
    
    ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
    Sans index+/- 2s+/- 2s0Mb
    index date_status+/- 2ms+/- 310ms160Mb
    index date+/- 2ms+/- 300ms120Mb
    index status+/- 2s+/- 100ms45Mb
    index date + index status+/- 2ms+/- 300ms160Mb
    index status_date+/- 2s+/- 3ms120Mb

    Ca y est nous avons enfin trouvé un index qui fonctionne pour la deuxième requête. Mais nous y perdons sur la première requête.

    C’est bien la preuve que l’ordre des colonnes d’un index composé est très important.

    Si nous reprenons notre arbre, voici l’explication.

    Visuellement on voit tout de suite pourquoi l’index marche bien.

    Très peu de traits rouges, le chemin vers les feuilles est très direct.

    Nous pouvons en tirer une nouvelle bonne pratique un index composé efficace commence par les colonnes dont les valeurs ont les plus faibles cardinalités : le moins de valeurs possibles.

    Utiliser un index partiel

    Certains SGDB comme MongoDB et PostgreSQL permettent de spécifier des conditions sur un index. Ca devient particulièrement intéressant à combiner avec un index composé qui sera tunné précisément pour une requête. Ca permettra de diminuer l’empreinte de l’index sur l’espace de stockage.

    CREATE INDEX "partial_status_date" 
    ON "runs" ("status", "date")
    WHERE status = "error";
    

    Cet index va indexer runs de la même façon que l’index composé sur “status”, “date” mais uniquement pour les enregistrements qui matchent la condition du WHERE.

    ScénarioTemps d’exécution requête dateTemps d’exécution requête runs en error entre deux datesTaille des index
    Sans index+/- 2s+/- 2s0Mb
    index date_status+/- 2ms+/- 310ms160Mb
    index date+/- 2ms+/- 300ms120Mb
    index status+/- 2s+/- 100ms45Mb
    index date + index status+/- 2ms+/- 300ms160Mb
    index status_date+/- 2s+/- 3ms120Mb
    index partial_status_date+/- 2s+/- 6ms1Mb

    C’est une vraie bonne nouvelle !

    Pour 1Mb de stockage, nous avons un index qui va adresser très correctement la requête voulue.

    En conclusion, les bonnes pratiques de la création d’index composé

    Les leçons que nous avons tirées de notre exercice sont :

    • l’ordre des colonnes d’un index composé est important
    • un index composé vraiment efficace est souvent un index réfléchi pour une et une seule requête
    • un bon index composé occupe environ le même espace que les index uniques qui le compose
    • Dans le doute, on peut appliquer la règle suivante : un index composé efficace commence par les colonnes dont les valeurs ont les plus faibles cardinalités
    • Les index partiels sont souvent un bon compromis, pour une taille qui peut être très réduite ils permettent de multiplier les index.

    Maintenant j’espère que les index composés vous semblent beaucoup moins magiques.

    La stratégie de benchmark

    Pour valider nos futures hypothèses nous allons utiliser une table témoin contenant 10 millions de runs.

    Chaque enregistrement a le format suivant :

    ColonneTypevaleur
    dateDatedate de type auto increment
    statusTextun enregistrement sur 100 en échec (⚠️ dans les graphes on a un échec sur 3 pour que ce soit plus compréhensible)

    Les benchmarks ont été fait sur mongodb, mais peuvent s’appliquer à PostgreSQL aussi.

    Ressources

    Repository github contenant le benchmark : https://github.com/jtassin/blog-post-composed-index-1

    Partial index sur postgres

    Partial index sur mongodb

  • REX : comment mener une activité annexe en parallèle du salariat

    “Y’a pas que les ordinateurs qui sont multitâches”

    Ou, un retour d’expérience sur mes activités parallèles au salariat.


    Web developer who also develops film.

    👆Ainsi commence ma bio Twitter.

    Pour les moins anglophones d’entre nous, ça arrive, on pourrait le traduire maladroitement en “développeur informatique qui développe aussi des photos”.

    Forcément, au royaume des 140 caractères, la brévité est de mise et cette description de mon quotidien est un peu réductrice. Mais elle n’en reste pas moins très juste !

    L’informatique est mon métier ainsi qu’une partie de mes loisirs, et la photographie argentique est ma passion ainsi qu’une partie de mes activités professionnelles.

    Ce continuum travail / profession / loisirs, dans un monde de plus en plus fluctuant, de plus en plus (inter)connecté est une facette nouvelle d’un schéma du travail fondamentalement opposé à la construction traditionnelle de l’individu par sa profession et à fortiori par une carrière longue et unique.

    “freelances”, “slashers”, entrepreneurs individuels, auto-entrepreneurs, “micro-taskers”, autant de formes de travail à l’opposé du CDI à vie et qui semblent pourtant se pérenniser.

    Moi-même c’est mon 4ème contrat en tant que développeur web, dans autant d’entreprises différentes. Là où mon père passa quelques 35 ans de carrière dans l’informatique au sein de la même société. Et sans la photographie et l’auto-entreprise, je n’aurais jamais eu l’occasion d’écrire ces lignes, car je ne serais jamais devenu développeur.


    Revenons au début. Ou presque, ça prendrait trop longtemps.

    2008, pas trop mauvais au lycée, bac S et direction l’INSA Lyon pour étudier le génie mécanique et sans le moindre doute y dédier ma carrière.

    2010, pas trop mauvais en photo, que je pratique depuis quelques années, je décide de les publier sur internet, et de faire un blog. En bon ingénieur que je n’étais pas encore, j’en profite pour essayer de coder ce blog moi-même, et mets un pied dans WordPress.

    2011, pas trop mauvais en WordPress, autodidacte et très motivé, je crée mon auto-entreprise pour fournir des sites vitrines à des connaissances et y consacre une année blanche.

    2013, plus trop motivé par le génie mécanique, et bien plus intéressé par l’informatique, je me réoriente vers un DUT Informatique en année spéciale.

    2014, pas déçu par ce DUT, j’enchaîne sur un cursus d’ingénieur à nouveau. En alternance et en informatique cette fois-ci.

    2017, finalement BAC+5, je quitte l’alternance et rejoins une startup où je choisis le temps partiel pour continuer la photo sur mon temps perso.

    2019, toujours passionné de photo, je prends le statut légal d’artiste auteur.

    2019 aussi, je rejoins Indy, alors Georges.tech, et je conserve ce temps partiel.


    C’est bien beau tout ça, mais quand est-ce qu’on mange pourquoi on lit ça sur un blog tech ?

    Parce que chez Indy, la tech ne s’arrête pas au point-virgule final de nos fichiers (oui, ici c’est semi: ['error', 'always']). On recrute et travaille quotidiennement avec des humains, et c’est la base de notre philosophie. Pour paraphraser notre annonce pour le poste de développeur·euse :

    Chez nous, les valeurs ce n’est pas juste du wording à la mode. Elles font partie intégrante de notre culture. Nous recrutons des talents H.A.P.P.Y.*

    * : Humble, Attentionné·e, Passionné·e, Perfectionniste, Y-factor

    Facile à dire, non ?

    Pas si facile dans la vraie vie (IRL™️), et encore moins dans une entreprise en hyper-croissance qui voit arriver de nouveaux visages tous les mois !

    Pourtant mon quotidien de développeur chez Indy en bénéficie tous les jours, et je suis heureux de rendre la pareille dès que j’en ai l’occasion.

    💡 Car c’est réellement important.

    À tel point que dans les attentes de progression de carrière, on trouve ces valeurs au même niveau que l’architecture logicielle ou la connaissance de Node.js.

    Et tout à l’heure, j’ai répondu “J’encourage les équilibres pro/perso, fais attention à ce que chacun trouve sa place.” en face du Passionné de HAPPY.

    Et je me suis dit que oui, en effet, je n’étais pas qu’un développeur informatique dans une équipe technique.

    Allez, assez de bla-bla. Des faits !

    • Je suis en temps partiel.
    • Ou plutôt, en temps partiels, 80% / 20%.
    • J’utilise mes compétences acquises à Indy pour mon activité de photographe, entre autres.
    • J’utilise mes compétences de photographe chez Indy. Littéralement. J’y prends régulièrement des photos.
    • Mais pas que. Je participe aussi aux explorations produit, étant très bien placé pour connaître les besoins des indépendants.
    • J’ai un statut légal d’auteur photographe, donc techniquement j’ai deux activités professionnelles.
    • Je profite des refontes de mon site pro pour faire de la veille technologique.
    • Évidemment, j’utilise Indy (avec plaisir) pour ma compta photo 😜.
    • Et bien sûr je peux débugger les features sur un “vrai” compte !

    J’avais commencé mon contrat en 90% sous un format un vendredi sur deux, mais je me suis vite rendu compte que c’était une galère à organiser. Les collègues ne savaient jamais si j’étais au bureau ou au bar, je devais prévoir chaque mois les jours de présence en jonglant entre les différents mois selon le nombre de vendredis effectifs, bref, c’était pas idéal.

    Aujourd’hui (et depuis un bon moment du coup), je suis en 80% avec un format habituel jamais d’Indy le vendredi (oui ça rime). Sachant que ce n’est pas gravé dans le marbre chez nous, et que quand et si j’ai besoin d’adapter ça ne pose pas de souci. Par exemple un shoot photo qui tombe le mardi ne sera pas perdu, et je rattraperai ma journée le vendredi. Mais c’est quand même plus simple pour tout le monde quand c’est un jour fixe.


    D’ailleurs puisqu’on parle de simplicité, il faut bien avouer que le temps partiel — comme tous les compromis — n’est pas l’organisation la plus simple. Mais à mes yeux la balance finale penche clairement du côté du bénéfice malgré de nombreuses petites frustrations.

    Parce que oui, même si on met pas en prod le vendredi il se passe quand même des choses au bureau sur Slack | Github à la tech Indy. Je pense notamment aux évènements et rituels de l’équipe qui ont lieu le vendredi et auxquels je ne peux donc pas participer.

    💡 Rappel : pour les tech Indy, le vendredi après-midi c’est veille techno, montée en compétences et R&D, 10% du temps de travail y étant dédié.

    Et je rate donc le goûter du code, chouette moment de partage informel entre nous. Certes il est enregistré, mais j’ai rarement la disponibilité le reste de la semaine pour rattraper. Et c’est quand même moins gratifiant sans les interactions.

    Pour la même raison, je fais mon temps de veille le jeudi après-midi afin de profiter d’une fin de semaine agréable ce qui me met en décalage du reste de la vie tech sur cette période.


    Aujourd’hui je me considère chanceux. Chanceux de l’équilibre que j’ai construit entre pro et perso, chanceux d’avoir trouvé une société qui encourage la diversité et la passion, au sein d’une Société où c’est loin d’être gagné d’avance. Chanceux d’avoir un métier épanouissant en parallèle de mes passions.

    Je dis chanceux, mais ça n’a rien à voir avec la chance dans les faits. Ça relève de l’organisation, de la philosophie de vie et de la bonne volonté de chacun. C’est un cercle vertueux, moins connu que son antagoniste vicieux mais tout aussi potent.

    Et il faut bien dire que ça demande aussi pas mal de boulot ! Pas simple de jongler entre différents agendas. D’où l’intérêt de bénéficier d’une politique congés souple et de bien s’organiser !

    Et vous alors ? Des passions envahissantes ? Des loisirs professionnalisés ? Des projets à entreprendre ?

    Vous faites quoi à côté du travail ? Comment gérez-vous votre temps précieux ?

    Vous avez forcément quelque chose d’autre à investir et faire fructifier. Et dans le cas contraire, vous trouverez bien un jour 🙂

Quitter la version mobile