Aller au contenu
English
Cloud medium

Provoquer une panne d'API et vérifier que votre code y survit

Read this page in English

15 min de lecture

Un 200 s'obtient partout. Un 503 reproductible, non. C'est pourtant sur les refus que reposent le retry, le backoff et la gestion d'erreur de votre code, et un vrai cloud ne vous les fournit ni à la demande ni deux fois de suite à l'identique. feint les arme en une requête : vous nommez l'opération, le statut et le nombre de fois, puis vous regardez ce que le client officiel en fait.

À la fin de cette page, terraform apply aura traversé deux 503 puis réussi, vous saurez lire le refus dans le dialecte de chaque fournisseur, et vous distinguerez une panne de statut d'une réponse tronquée.

  • Armer une panne déterministe sur une opération nommée.
  • Observer la réaction réelle du client, au lieu de la supposer.
  • Distinguer le refus, la lenteur et la réponse coupée en deux.
  • Dire ce que ce test démontre, et ce qu'il laisse entier.

Pourquoi une panne est plus difficile à obtenir qu'un succès

Section intitulée « Pourquoi une panne est plus difficile à obtenir qu'un succès »

Le chemin nominal de votre code est testé par accident : chaque exécution le parcourt. Le chemin de dégradation, lui, n'est parcouru que le jour où le fournisseur a un incident, c'est-à-dire au pire moment et sans témoin. Les équipes contournent ce trou en simulant la panne dans les tests unitaires du client, ce qui vérifie une hypothèse sur l'API plutôt que l'API.

L'injecteur de feint déplace la frontière : le vrai client officiel, son vrai décodeur d'erreurs et sa vraie politique de retry traversent un refus que vous avez décidé. Ce que vous mesurez alors n'est plus une hypothèse, c'est un comportement observé du client que vous embarquerez en production.

L'exemple prend l'opération que tout le monde appelle en premier chez Scaleway, instance/v1/API.ListServers, et la fait échouer exactement deux fois.

  1. Démarrer l'émulateur et pointer le client officiel dessus.

    Fenêtre de terminal
    feint start
    eval "$(feint env scaleway)"

    La commande feint env exporte les variables que le SDK Scaleway attend, endpoint compris. Le détail de ce qu'elle pose est dans le guide Brancher les CLI officiels.

  2. Armer la règle.

    Fenêtre de terminal
    curl -X PUT localhost:4599/_feint/faults -d '{"faults":[
    {"operation":"instance/v1/API.ListServers","status":503,"times":2}]}'

    L'émulateur répond la liste des règles armées, chacune avec son compteur hits à zéro et son drapeau spent à false. Rien d'autre n'est modifié : les autres opérations continuent de répondre normalement.

  3. Appeler, et lire le refus.

    Fenêtre de terminal
    scw instance server list zone=fr-par-1
    Feint answered Service Unavailable because a fault rule is armed for this
    operation; GET /_feint/faults lists them

    Le corps porte sa propre origine. C'est délibéré : un message qui imiterait un incident Scaleway inventerait un fait sur le fournisseur, et vous ne sauriez plus, six mois plus tard, si ce 503 venait d'un test ou d'une panne.

  4. Vérifier que la règle a bien tiré, et combien de fois.

    Fenêtre de terminal
    curl -s localhost:4599/_feint/faults | jq '.faults[] | {operation, status, times, hits}'
    {
    "operation": "instance/v1/API.ListServers",
    "status": 503,
    "times": 2,
    "hits": 1
    }

    Ce compteur est le cœur du test. Un client qui abandonne au premier refus laisse hits à 1 ; un client qui retente laisse 2, puis reçoit la vraie réponse au troisième appel. Vous n'avez pas à croire la documentation du SDK sur sa politique de retry, vous la lisez.

  5. Désarmer.

    Fenêtre de terminal
    curl -X DELETE localhost:4599/_feint/faults

    Une règle oubliée est une panne fantôme dans le test suivant. Le DELETE vide l'ensemble et répond la liste vide.

Ces propriétés ne sont pas des intentions, ce sont des refus écrits dans le code, et chacune se vérifie depuis votre terminal.

Éteint par défaut. Un émulateur fraîchement démarré n'arme rien. Aucun test ne peut donc échouer à cause d'une règle héritée d'ailleurs.

Déterministe. La règle dit « les N premiers appels de cette opération », jamais « une fois sur dix ». Une panne aléatoire ne peut pas être le sujet d'un test : elle produit un échec que le rejeu ne reproduit pas.

Par opération. La cible est le nom amont publié par /_feint/routes. Une règle qui vise une opération que personne ne sert est refusée à l'écriture, pas silencieusement ignorée :

Fenêtre de terminal
curl -X PUT localhost:4599/_feint/faults -d '{"faults":[
{"operation":"instance/v1/API.CetteOperationNExistePas","status":503,"times":1}]}'
HTTP 400
no route serves the operation "instance/v1/API.CetteOperationNExistePas":
a rule that never fires reads exactly like a client that survived the fault

La phrase dit le piège qu'elle évite : une règle qui ne tire jamais ressemble trait pour trait à un client qui a résisté. Sans ce refus, vous concluriez que votre code est robuste alors que rien ne l'a testé.

Dans le dialecte du fournisseur. Un refus n'a pas la même forme selon le cloud, et c'est précisément ce que votre code doit savoir décoder.

Trois enveloppes, un seul mécanisme. Le tableau ci-dessous est relevé sur la version 0.13.0, en armant la même règle sur chaque pack.

FournisseurCe que le client reçoitCe que votre code doit reconnaître
Scalewayscaleway-sdk-go: insufficient permissions sur un 403le type d'erreur du SDK, ici PermissionsDeniedError
Outscale{"Errors":[{"Type":"FeintInjectedFault",…}],"ResponseContext":{…}}l'enveloppe Errors et son champ Type
Exoscale{"message":"feint answered Service Unavailable…"}un message nu, sans code exploitable

Cette asymétrie est celle du vrai cloud, pas une facilité de l'émulateur : l'enveloppe d'Outscale prévoit un champ pour un marqueur, celle d'Exoscale n'en prévoit aucun. Un code qui branche sur le message texte fonctionnera chez l'un et cassera chez l'autre, et c'est ce que ce test rend visible avant la production.

Chaque pack ne rend que les statuts qu'il sait écrire. Demander un statut hors de cette liste est refusé, avec la liste :

rule "instance/v1/API.ListServers": the scaleway pack does not render 418;
it renders [401 403 429 500 502 503]

Au-delà du statut : la lenteur et le corps coupé

Section intitulée « Au-delà du statut : la lenteur et le corps coupé »

Une API ne tombe pas toujours franchement. Elle traîne, ou elle coupe une réponse en deux, et ces deux pannes cassent des clients qui survivent très bien à un 503 propre.

Le délai se déclare en durée Go et s'applique avant la réponse :

Fenêtre de terminal
curl -X PUT localhost:4599/_feint/faults -d '{"faults":[
{"operation":"instance/v1/API.ListServers","delay":"3s","times":1}]}'

Mesuré sur cette machine, le premier appel prend 3,010 s et le suivant 0,007 s : la règle est épuisée et l'émulateur repasse à sa vitesse normale. C'est la forme d'une API qui pend, celle qui révèle un client sans timeout. Un délai aberrant est borné plutôt qu'accepté, parce qu'une valeur de ce genre est presque toujours une faute de frappe :

rule "…": delay 30m is longer than 5m0s, which is a typo more often than an intent

Le corps tronqué garde le statut du gestionnaire et coupe la réponse à N octets. Le client officiel, lui, ne s'en remet pas, et le message qu'il produit est celui que vous verrez en production :

Fenêtre de terminal
curl -X PUT localhost:4599/_feint/faults -d '{"faults":[
{"operation":"instance/v1/API.ListServers","truncate_bytes":12,"times":1}]}'
scw instance server list zone=fr-par-1
scaleway-sdk-go: could not parse application/json response body: unexpected EOF

Un 200 accompagné d'un JSON incomplet est le scénario du proxy qui coupe, de la passerelle qui expire, de la page interrompue. Il ne se produit jamais dans un test heureux, et il produit ici une erreur nommée que votre code peut apprendre à traiter.

Comment tester un client qui attend qu'une ressource soit prête

Section intitulée « Comment tester un client qui attend qu'une ressource soit prête »

Depuis la 0.13.0, l'option --consistency eventual fait passer les ressources par les états transitoires d'un vrai cloud, ce qui donne enfin quelque chose à observer au code qui attend. Par défaut, l'émulateur règle chaque action d'un coup : un volume naît available, un serveur redémarré répond running de la première lecture à la dernière. C'est rapide et déterministe, mais une boucle d'attente n'y rencontre jamais l'état qu'elle guette, et un défaut dans cette boucle passe inaperçu.

Fenêtre de terminal
feint serve --consistency eventual

Le contraste se mesure en deux commandes. Sans l'option, un volume Outscale est disponible immédiatement ; avec elle, il naît creating et la lecture suivante le trouve available.

Fenêtre de terminal
octl iaas api CreateVolume --Size 10 --SubregionName eu-west-2a --VolumeType gp2 --jq '.Volume.State'
creating
Fenêtre de terminal
octl iaas api ReadVolumes --jq '.Volumes[-1].State'
available

Les états avancent à la lecture, jamais sur une horloge. C'est un choix de conception plutôt qu'un raccourci : une transition minutée dormirait indéfiniment sous une horloge de test figée, ou transformerait chaque suite en salle d'attente. Une suite de quatre secondes reste une suite de quatre secondes, et le nombre de lectures nécessaires est reproductible d'une exécution à l'autre.

Ce mécanisme rend atteignables des refus qui n'existaient pas ici. Un snapshot demandé pendant que le volume est encore creating reçoit la réponse mesurée sur un vrai compte, avec son statut, son type et son code.

{"Errors":[{"Code":"6007","Details":"the volume vol-a88ea433 is still creating; a snapshot can be taken once it is available","Type":"InvalidVolumeState"}]}

Côté Scaleway, ce sont les actions de cycle de vie qui deviennent observables, dans l'ordre relevé sur fr-par : poweron répond starting puis running, poweroff répond stopping puis stopped, et reboot répond stopping, starting, puis running. Ce dernier cas est celui qui justifie tout le mécanisme : l'état visé par un redémarrage est celui d'où l'on part, donc un client qui se contente de vérifier state == "running" croit avoir attendu alors qu'il n'a rien attendu du tout.

Deux garde-fous méritent d'être connus, parce qu'ils décident de ce que votre test prouve. Une action n'est pas une observation : la lecture qu'une action effectue pour modifier la ressource ne consomme pas d'état, sinon le redémarrage avalerait le stopping qu'il vient de poser. Et une action échouée ne raconte aucun parcours : un démarrage qui a échoué répond son état d'échec, sans narrer une route vers un running qu'il n'a jamais atteint.

C'est la distinction la plus importante de cette page, et elle est inscrite dans les compteurs de l'émulateur.

Ce que vous venez de démontrer est un fait sur votre client : que scw décode un corps permissions_denied comme un refus de droits plutôt que comme une ressource absente, que terraform apply survit à deux 503 sur une lecture, que votre code sort proprement quand le JSON arrive tronqué. Ce sont les faits dont vous avez besoin, et ils étaient inobservables autrement.

Ce que vous n'avez pas démontré est un fait sur le vrai cloud. Un 403 injecté ici n'établit pas que Scaleway répond 403 à cet appel, ni avec ces champs. La forme du corps vient du SDK du fournisseur, mais le choix du moment vient de vous.

SymptômeCauseSolution
HTTP 400 à l'armement, message « no route serves the operation »Le nom de l'opération est faux ou l'émulateur ne la sert pasLister les noms exacts : curl -s localhost:4599/_feint/routes
La règle est listée mais hits reste à 0Le client appelle une autre opération que celle viséeLire le journal des appels : curl -s localhost:4599/_feint/trace
HTTP 400, « the pack does not render 418 »Le statut demandé n'a pas de forme dans le dialecte du packChoisir dans la liste que le message affiche
Un test suivant échoue sans raisonUne règle est restée arméecurl -X DELETE localhost:4599/_feint/faults en fin de test
Le client attend indéfinimentUn delay est armé et le client n'a pas de timeoutC'est le défaut cherché : ajouter un timeout côté client
  • Une panne s'arme en une requête : opération, statut, nombre de fois.
  • Le déterminisme est la condition du test : « les N premiers appels », jamais une probabilité.
  • Une règle qui ne peut pas tirer est refusée à l'écriture, car elle ressemblerait à une réussite.
  • Le refus arrive dans le dialecte du fournisseur, et les trois dialectes diffèrent.
  • Statut, lenteur et corps tronqué sont trois pannes distinctes, et la troisième casse d'autres clients que la première.
  • Une réponse injectée ne prouve rien sur le vrai cloud et ne compte dans aucun indicateur de fidélité.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn