Tout limiter dans l’espace et dans le temps

Je visionnais la conférence donnée par M. François-Guillaume Ribeau — Votre API web passe-t-elle les 50 points du contrôle technique — et un des principes en particulier mérite à mon sens une attention toute particulière : « Tout limiter dans l’espace et dans le temps. » M. Ribeau en parle dans le contexte de la pagination des API web mais nous allons voir dans cet article que cette doctrine peut s’appliquer dans bien d’autres situations.

Introduction

Prenons l’exemple de Justine, développeuse d’une API pour une entreprise qui fabrique et livre des pièces détachées et des accessoires de vélo. L’entreprise de Justine la pousse à livrer des fonctionnalités rapidement et elle a la « tête dans le guidon » (héhé). Durant le sprint, Justine doit implémenter une API qui liste les pièces disponibles ainsi que leur tarif, délai de livraison, etc.

Les tests sont écrits sans fioritures, la requête SQL est simple — à l’image du format de retour.

-- J’utilise ici SQL mais le principe le même peu importe la base de données.
SELECT `id`, `name`, `stock`, `price`
  FROM `catalog`
 WHERE `stock` > 0
{
  "results": [
    {
      "id": "c311eece-26aa-427d-a749-6e630039a85f",
      "name": "Kit câble et gaine de frein",
      "stock": 13,
      "price": { "amount": 399, "currency": "EUR" }
    },
    {
      "id": "0c0b78a9-d6c1-48cc-904f-e52e423785f6",
      "name": "Vis de pédalier",
      "stock": 254,
      "price": { "amount": 249, "currency": "EUR" }
    }
  ]
}

Son Product Owner ne voit pas venir le loup et Justine n’a pas suffisament de recul pour y penser ; l’instant d’après la fonctionnalité arrive en production.

Deux mois plus tard, un investisseur est intégré dans l’entreprise, qui connaît une croissance importante. À cette occasion, des milliers de produits sont intégrés dans le catalogue. Plusieurs problèmes sont alors révélés :

  • la requête SQL prend un certain temps à s’exécuter 
  • cela monopolise une connexion à la base de données 
  • le serveur a une charge de travail plus conséquente pour traiter et formater tous les résultats 
  • la bande passante nécessaire pour renvoyer ces derniers au client augmente linéairement avec le nombre de produits dans le catalogue 
  • les divers proxies et services intégrant potentiellement l’API de Justine sont eux aussi mobilisés à attendre 
  • tout comme le serveur, le client a aussi une charge de travail plus conséquente pour traiter et afficher la réponse 
  • dans l’ensemble, plus on intègre de produits, plus la requête prend du temps.

Justine a commis une erreur : elle n’a pas limité son API dans l’espace, ni le temps.

Limiter dans l’espace

Régler naïvement le problème

On comprend tout a fait que la pagination ne soit pas à l’ordre du jour en début de projet. N’en déplaise à M. Ribeau, son équipe ne lui a pas laissé la possibilité de valider tous les points du « contrôle technique ».

Justine aurait néanmoins pu, simplement et en une ligne, limiter sa requête API dans l’espace, c’est à dire limiter le nombre de résultats traités et renvoyés par sa base de données.

SELECT `id`, `name`, `stock`, `price`
  FROM `catalog`
 WHERE `stock` > 0
 LIMIT 200

Cette « solution » n’est pas parfaite car si elle résoud les problèmes évoqués, elle en pose un de taille : on ne retournera, au maximum, que 200 produits. Notez que cette limite est arbitraire et dépend du métier — 100 produits, 200 produits, 1000 produits — cela n’importe que peu.

Tout ce qui compte, c’est de définir une limite.

Faites une analyse des risques. Est-il plus grave pour votre entreprise de ne proposer que 100, 200 ou 1000 produits à la vente sur plusieurs milliers, ou de fournir au mieux une expérience client pénible et au pire un site hors-ligne ?

Il n’y a pas une réponse qui s’applique à 100 % des cas mais il y a fort à parier que vous voudriez vous trouver dans la première catégorie.

Encore une fois la solution est loin d’être parfaite, et à l’instar de M. Ribeau vous préféreriez une API paginée. Figurez-vous que le Product Owner de Justine aussi, il a simplement remis cette fonctionnalité « à plus tard », c’est à dire dans les tréfonds du backlog avec les autres fonctionnalités nice to have. En attendant, son site est inopérationnel.

Rester au courant

Il y a clairement un manque de communication et de planification dans l’équipe de Justine. C’est très certainement aussi le cas de la vôtre. Un autre des points du « contrôle technique » est la mise en place de la surveillance de vos API et d’alarmes se déclenchant lorsque certains seuils sont atteints.

Ici, on pourrait imaginer une alarme qui se déclenche lorsque plus de 90 % de la limite est atteinte — 180 produits sont retournés. Je vous laisse imaginer comment implémenter celle-ci : surveillance du volume de la base de données, un simple if (results.length > 180) { … } dans le code, etc.

Une fois ce seuil franchi, l’équipe est au fait des limites de son produit et peut alors prioriser la mise en place de la pagination en connaissance de cause.

Limiter dans le temps

Une deuxième facette reste à prendre en compte : la durée de chaque opération. Plus une application a d’acteurs, plus les risques de mauvaise communication, erreurs de transmissions, gigue ou latence élevées ou autres augmentent. Reprenons l’exemple de Justine : que se passera-t-il si une erreur réseau occasionnelle se produit lorsque son serveur applicatif essaye de récupérer des informations dans sa base de données ?

La plupart des bibliothèques autorisent le paramétrage de timeouts, de délais passés lesquels les tentatives de connexions se transforment en erreurs. Certaines proposent même des timeouts par défaut. Cependant, les valeurs par défaut de ces timeouts ne sont probablement pas adaptées à votre utilisation. À titre d’exemple, la bibliothèque mysql pour Node.js propose un timeout par défaut de 10 secondes pour les connexions à la base de donnée mais n’en définit aucun pour le traitement des requêtes SQL elles-mêmes — bien qu’il soit possible d’en préciser un.

Vous vous en doutez, il y a bien plus de garde-fous sur l’occupation temporelle que sur l’occupation spatiale. Il est probable que chacun des acteurs de la chaîne dispose de timeouts, de la base de donnée au navigateur de votre visiteur. Cependant, une application bien conçue est aussi une application bien configurée, et le paramétrage des timeouts est souvent une tâche prise à la légère.

Dressez un schéma de votre architecture, et posez-vous la question suivante : « Combien de temps puis-je me permettre de passer entre ces deux composants ? ». Puis définissez ou modifiez vos timeouts !

Un timeout dépassé est signe d’un problème sous-jacent. Profitez-en pour mettre en place des alertes si ce n’est pas déjà fait. Soyez proactifs !

Mise en perspective

Je vais maintenant vous demander d’oublier l’exemple que nous exploitons depuis le début de cet article pour réfléchir au problème de façon plus générale. Comprenez bien qu’il ne s’agit pas « que » de mettre en place une pagination sur votre API, ni « juste » de définir des timeouts lors de requêtes à votre base de données.

Vous devez y penser de manière globale.

Votre serveur doit être configuré pour ne pas autoriser les requêtes dont le poids dépasse un certain seuil. À moins que vous ne travailliez sur un projet avec des spécificités bien particulières, votre application ne devrait pas passer plus de quelques secondes sur le traitement d’une requête. Le payload JSON que vous acceptez en entrée de votre API a probablement une taille limite, passée laquelle vous devriez tout simplement la refuser — en passant, gardez en tête que la désérialisation JSON avec JSON.parse() est synchrone. Un trop gros corps de requête va littéralement incapabiliser votre serveur pendant le temps de traitement.

Si votre application accepte des entrées utilisateur — y compris si ces utilisateurs sont authentifiés, administrateurs, etc. — limitez leur taille. Si votre application fait appel à des API externes, mettez en place des timeouts — ou encore mieux, des circuit breakers. Gardez en tête le volume de données à traiter et le temps de traitement de chaque étape. Si votre application utilise des expressions régulières, méfiez-vous des modificateurs + et * ; préférez-leur ceux qui vous permettent de définir un nombre maximum de répétitions.

Testez votre application ! Et pas seulement avec des entrées prédéfinies : introduisez du hasard dans vos tests. Avec des bibliothèques de property-based testing (PBT), il vous est possible de générer des myriades de combinaisons différentes et de tailles variées, et probablement d’identifier les trous dans la raquette.

Conclusion

Le monde informatique a ceci de magique qu’il est pratiquement impalpable. Nous avons parfois tendance à oublier qu’un processeur, de l’espace disque, de la mémoire vive, ou de la bande passante sont des réalités physiques avec des limites fortes.

Lorsque ces limites nous sont inconnues, ou que nous n’avons pas les moyens d’agir dans l’immédiat, la meilleure action à prendre reste de mettre en place des alertes se déclenchant lorsqu’une valeur dépasse un seuil prédéterminé. Cela nous apporte, faute de mieux, de la visibilité.

Pour résumer, afin d’éviter les mauvaises surprises et les déconvenues, il est nécessaire de garder en tête les réalités physiques sous-jacentes à nos applications. Plus les vérifications sont faites en amont, plus les difficultés sont simples à gérer. Limitez tout dans l’espace et dans le temps.