Back home

La fiabilité de l'exécution de Rust/Wasm nécessite de gérer à la fois la récupération en cas de panique et d'abandon.

Une fois que l'instance Wasm partagée commence à accepter des appels pendant une longue période, le crash passera d'un simple échec à un problème de récupération d'état et d'isolation des pannes.

Wasm peut facilement être considéré comme une couche de portage au début : le code peut être programmé, la page peut s’exécuter, les performances sont correctes et les choses semblent être à peu près les mêmes. Cela commence vraiment à devenir difficile, généralement une fois la démo passée. Une fois que les modules tels que les éditeurs, les moteurs de rendu et les analyseurs de documents passeront des expériences sur une seule page aux environnements d’exécution résidents à long terme, les modèles de pannes changeront immédiatement.

À l’heure actuelle, la panique et l’abandon ne sont plus des branches d’exception dans la couche linguistique. Ce qu’ils décident, c’est : si cette instance peut continuer à recevoir des travaux ultérieurs, si l’état de la mémoire est contaminé, si la couche hôte doit supprimer l’instance immédiatement et si le pool d’instances doit être rempli. Lorsque l’équipe mobile déplace vers le Web un noyau qui s’exécute depuis longtemps dans des conteneurs natifs, c’est cette couche de changement qui est le plus facilement sous-estimée.

Une fois la démo réussie, le modèle de faute vient de démarrer.

Les plantages lors d’un seul appel ne sont pas difficiles à comprendre. Un clic sur un bouton déclenche un appel Wasm. En cas d’échec, une erreur sera signalée pour l’opération. Actualisez la page et réessayez. Le coût est toujours contrôlable.

Le problème se produit une fois que le runtime commence à réutiliser les instances. Lorsque la même instance Wasm ouvre continuellement plusieurs documents, reçoit plusieurs séries d’événements d’entrée et passe par plusieurs appels de pont JS, la portée de l’influence de la panique et de l’abandon ne s’arrête plus à l’action en cours. Un échec incomplet peut ralentir les demandes ultérieures.

Ces risques ne sont souvent pas exposés dès le premier jour. Dans la première étape, vous ne voyez généralement que des rapports d’erreurs dispersés : des échecs de rendu occasionnels, une certaine exportation est bloquée et un certain document est dans un état incorrect après avoir été fermé et rouvert. Si l’on vérifie plus profondément, les indices convergeront progressivement vers le même phénomène : même si la panne s’est produite dans une chaîne d’appels, le dommage est resté dans l’instance partagée.

À ce stade, la discussion n’est plus « Si le code Rust va paniquer », mais « Si ce runtime est qualifié pour continuer à servir le prochain appel après la panique ».

La panique peut être attrapée, l’abandon ne peut que modifier les instances.

La chose la plus importante à séparer dans Rust/Wasm sont les deux sémantiques d’échec que sont la panique et l’abandon.

La panique a également la possibilité de se détendre le long des limites établies. Tant que la couche de liaison et la couche hôte s’accordent à l’avance sur la méthode de récupération, l’appel en cours peut échouer et d’autres états de l’instance peuvent également être conservés. l’abandon n’est pas du tout la voie à suivre. Cela signifie que l’exécution en cours a atteint un état irrécupérable. Si vous continuez à utiliser la même instance pour recevoir des requêtes, vous pariez essentiellement que la mémoire et les ressources ne seront pas endommagées à mi-chemin.

Une fois les deux mélangés pendant l’exécution, des problèmes surviendront certainement lors du traitement ultérieur :

  • Abandonnez l’opération comme une exception normale, et le pool d’instances continuera à réutiliser les objets qui ont perdu leur crédibilité.
  • Traitez toutes les paniques comme si l’instance devait être détruite, et le débit serait inutilement réduit.
  • L’hôte JS sait seulement “l’appel a échoué”, mais ne sait pas s’il doit réessayer, perdre l’instance ou interrompre la session en cours

C’est également l’aspect le plus réaliste de la fiabilité de l’exécution de Wasm : la sémantique de récupération doit d’abord être définie avant que l’isolation et la planification ultérieures puissent être mises en œuvre.

Si la couche de liaison ne fournit pas de sémantique de récupération, la couche hôte prendra le mauvais état et continuera à accepter le travail.

L’endroit le plus dangereux pour ce genre de problème n’est pas dans le code des affaires, mais dans la couche contraignante qui semble avoir été “déjà réglée”. La couche hôte ne voit souvent qu’un objet d’erreur émis et l’enregistre comme un échec d’appel normal. Le journal est là et la page ne plante pas immédiatement, mais le système a peut-être laissé le mauvais état à l’intérieur de l’instance.

Ce qui doit vraiment être corrigé, ce n’est pas seulement try/catch, mais les actions de gestion après un échec. Une logique similaire à la suivante vient tout juste de commencer à entrer dans la conception de la fiabilité :

async function runWithRecovery(instance, input) {
  try {
    return await instance.exports.handle(input)
  } catch (error) {
    if (isAbort(error)) {
      pool.replace(instance.id)
    }
    throw error
  }
}

Ce code ne se concentre pas sur la syntaxe, mais sur un simple jugement : si l’échec actuel a marqué cette instance comme un objet non fiable. Si la réponse est oui, l’action de récupération ne doit pas s’arrêter à la génération d’erreurs, mais doit se poursuivre jusqu’à l’élimination de l’instance, la reconstruction des ressources et la réduction du flux de requêtes.

Tant que cette couche n’est pas clairement définie, le système semblera gérer les erreurs, mais ce qu’il fait en réalité, c’est remettre un runtime potentiellement corrompu dans le chemin de production.

Les instances partagées amplifieront le problème de récupération en un problème de stratégie de pooling

Une fois Wasm intégré dans de vrais produits, il n’y a rarement qu’une seule « instance jusqu’à ce que la page soit fermée ». Les pools d’instances, les pools de tâches ou les documents de premier plan et tâches d’arrière-plan partageant un ensemble de ressources d’exécution sont plus courants. À ce stade, les coûts de récupération liés à la panique et à l’avortement réécriront directement la stratégie de mutualisation.

Si l’initialisation d’une instance coûte cher, le système aura naturellement tendance à la réutiliser autant que possible. Mais une fois la réutilisation établie, l’isolation des pannes doit être améliorée simultanément :

  • Quels états ne peuvent être suspendus qu’au cours d’un seul appel et seront supprimés avec l’appel après un échec
  • Quels caches peuvent être conservés lors des appels et quels caches doivent être complètement invalidés en cas d’abandon
  • Une fois l’instance remplacée, comment les tâches en file d’attente seront-elles migrées ? Réessayer entraînera-t-il des effets secondaires deux fois ?

Ce ne sont pas des réponses que la couche linguistique enverra automatiquement. Ce sont des conceptions d’exécution.

Pour cette raison, si la discussion sur la fiabilité de Rust/Wasm s’arrête uniquement à « la panique peut-elle être attrapée ? », il est facile de sous-estimer le problème. Ce qui creuse réellement l’écart en matière de coûts de maintenance, c’est la capacité du pool d’instances à maintenir une limite de confiance claire après une panne.

La limite applicable est fortement liée au cycle de vie

Cet ensemble de conceptions de restauration n’est pas requis pour tous les projets Wasm.

Si le module n’est qu’un outil hors ligne ponctuel ou si l’instance entière est recyclée lorsque la page est détruite, la différence entre panique et abandon existera toujours, mais le bénéfice de la récupération sera bien moindre. Il suffit souvent de rafraîchir directement la page et de réexécuter directement la tâche.

Une fois que le système aura les caractéristiques suivantes, la sémantique de récupération passera rapidement d’un « élément d’optimisation » à un « élément d’infrastructure » :

  • L’instance réside pendant une longue période et n’est pas détruite avec un cycle de vie d’une seule page
  • La même instance effectue continuellement plusieurs séries d’appels
  • La couche d’hébergement doit utiliser le pooling en échange du temps de démarrage et du débit
  • Protéger l’état de la session, l’état du cache et les tâches en file d’attente après un échec

Lorsque les équipes mobiles déplacent les fonctionnalités natives vers le Web, cette limite est la plus susceptible d’être rencontrée. La relation d’isolement initialement établie par défaut dans le processus d’application devait souvent être remplie à nouveau après avoir atteint la limite de l’hôte JS/Wasm.

Wasm facilite l’entrée du code natif dans le navigateur, mais il n’apporte pas de sémantique de récupération d’exécution. Dès que le système commence à partager des instances, à réutiliser l’état et à accepter des appels à long terme, la panique et l’abandon doivent être traités comme deux événements d’exécution différents. Le premier se soucie de la façon de mettre fin à l’appel en cours, et le second se soucie de savoir si cette instance peut continuer à vivre dans le pool. Si ce jugement n’est pas fait en premier, plus la transplantation de code est réussie, plus il sera difficile de faire face aux échecs ultérieurs.