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.
Ce que vous allez obtenir
Section intitulée « Ce que vous allez obtenir »À 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.
Le lab : deux 503, puis la réponse
Section intitulée « Le lab : deux 503, puis la réponse »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.
-
Démarrer l'émulateur et pointer le client officiel dessus.
Fenêtre de terminal feint starteval "$(feint env scaleway)"La commande
feint envexporte 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. -
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 drapeauspentàfalse. Rien d'autre n'est modifié : les autres opérations continuent de répondre normalement. -
Appeler, et lire le refus.
Fenêtre de terminal scw instance server list zone=fr-par-1Feint answered Service Unavailable because a fault rule is armed for thisoperation; GET /_feint/faults lists themLe 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
503venait d'un test ou d'une panne. -
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. -
Désarmer.
Fenêtre de terminal curl -X DELETE localhost:4599/_feint/faultsUne règle oubliée est une panne fantôme dans le test suivant. Le
DELETEvide l'ensemble et répond la liste vide.
Les quatre garanties de l'injecteur
Section intitulée « Les quatre garanties de l'injecteur »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 :
curl -X PUT localhost:4599/_feint/faults -d '{"faults":[ {"operation":"instance/v1/API.CetteOperationNExistePas","status":503,"times":1}]}'HTTP 400no route serves the operation "instance/v1/API.CetteOperationNExistePas":a rule that never fires reads exactly like a client that survived the faultLa 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.
Comment chaque cloud dit la même panne
Section intitulée « Comment chaque cloud dit la même panne »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.
| Fournisseur | Ce que le client reçoit | Ce que votre code doit reconnaître |
|---|---|---|
| Scaleway | scaleway-sdk-go: insufficient permissions sur un 403 | le 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 :
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 intentLe 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 :
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-1scaleway-sdk-go: could not parse application/json response body: unexpected EOFUn 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.
feint serve --consistency eventualLe 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.
octl iaas api CreateVolume --Size 10 --SubregionName eu-west-2a --VolumeType gp2 --jq '.Volume.State'creatingoctl iaas api ReadVolumes --jq '.Volumes[-1].State'availableLes é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.
Ce que ce test prouve, et ce qu'il ne prouve pas
Section intitulée « Ce que ce test prouve, et ce qu'il ne prouve pas »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.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause | Solution |
|---|---|---|
HTTP 400 à l'armement, message « no route serves the operation » | Le nom de l'opération est faux ou l'émulateur ne la sert pas | Lister les noms exacts : curl -s localhost:4599/_feint/routes |
La règle est listée mais hits reste à 0 | Le client appelle une autre opération que celle visée | Lire 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 pack | Choisir dans la liste que le message affiche |
| Un test suivant échoue sans raison | Une règle est restée armée | curl -X DELETE localhost:4599/_feint/faults en fin de test |
| Le client attend indéfiniment | Un delay est armé et le client n'a pas de timeout | C'est le défaut cherché : ajouter un timeout côté client |
À retenir
Section intitulée « À retenir »- 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é.