Le moteur qui alimente des millions d'exécutions Okta Workflows chaque jour a été l'un des premiers à adopter l'écosystème Rust asynchrone. Bien que Tokio soit le choix naturel pour un runtime asynchrone aujourd'hui, nous avons lancé Workflows avant la sortie de Tokio 1.0, et bien avant que les meilleures pratiques et modèles pour Rust asynchrone ne se soient réellement solidifiés. 

Cet article détaille la façon dont nous avons migré le moteur Workflows, une base de code de plus de 100 000 lignes de code source, de Tokio 0.1 basé sur les continuations vers async / await Tokio 1.0, sans interruption de service ni interruption du développement de fonctionnalités.

Futures basées sur les continuations

Au début, l'exécution asynchrone des futures Rust se faisait en transmettant des fonctions qui représentent des valeurs éventuelles, appelées continuations. Ces continuations étaient chaînées et chacune était exécutée de manière asynchrone par un runtime tel que Tokio, et finiraient par aboutir à la valeur souhaitée. Avant la migration, nous avions des centaines de milliers de lignes de code qui ressemblaient à ceci :
 

Du code avant la migration

 

C'était l'état de Rust asynchrone jusqu'à ce que la syntaxe async / await soit stabilisée. Pour Workflows, être un des premiers utilisateurs signifiait que nous devions livrer bien avant la stabilisation de async / await, et bien que ce type de code soit difficile à lire et à maintenir, nous l'avons poussé aussi loin que possible.

Il nous a bien servi, nous permettant de développer Okta Workflows jusqu'à son niveau actuel. Avance rapide d'environ un an, lorsque les fissures commençaient à devenir insupportables, et il est devenu nécessaire de commencer enfin la migration vers la syntaxe async / await moderne et Tokio 1.0.

Futures basées sur Async / await

Avec Tokio 1.0, le flux de contrôle asynchrone pouvait être écrit avec la syntaxe (désormais stable depuis longtemps) async / await, ce qui simplifie grandement le code et réduit le boilerplate. L'exemple ci-dessus pourrait être écrit comme ceci.

 

Même code avec Tokio 1.0

 

Même si vous n'êtes pas très familier avec Rust, les avantages de lisibilité et de simplicité de l'utilisation de async / await sont clairs. Pour ceux qui connaissent Rust, la capacité de async / await à emprunter à travers les points d'attente nous a permis de ne plus avoir besoin de transmettre la propriété de l'objet Flow, nous n'avions donc plus besoin de le stocker dans le FlowError. Ceci était crucial pour relayer les erreurs à nos clients, et une source de nombreux bugs lorsqu'un chemin de code oublie inévitablement de stocker le Flow dans une erreur. Ce ne serait qu'une seule classe de bugs que la migration vers async / await nous aiderait à éliminer.

Le grand saut 

Bien que les avantages de la migration vers Tokio 1.0 soient clairs, ce serait une tâche ardue de le faire. La migration n'est pas triviale et nécessiterait une réécriture complète du moteur de Workflows. Une réécriture complète et radicale est irréalisable et risquée, tant du point de vue commercial que de la fiabilité du client. Nous ne pouvions pas interrompre le développement du produit pour entreprendre une réécriture complète ; en effet, cela aurait été une erreur fatale pour tout produit logiciel

Pour compliquer encore les choses, au moment de notre enquête sur la réécriture, de nouvelles fonctionnalités étaient en préparation. Par exemple, nous allions sortir la possibilité d'annuler un Workflow quelques mois plus tard, en même temps que cet effort de migration vers Tokio 1.0. Une réécriture de cette ampleur devrait se faire petit à petit, comme si l'on remplaçait les roues d'un train en marche. 

Couches de compatibilité et indicateurs de fonctionnalité

Okta Workflows utilise la bibliothèque hyper HTTP pour communiquer avec le Web et effectuer des appels vers des services externes tels que Google Drive ou Amazon S3 : tout cela fait partie de ce qui rend Workflows si puissant. Cette bibliothèque fondamentale a été l'une des premières à être remplacée : nous avons remplacé hyper 0.12, alimenté par Tokio 0.1, par hyper 0.14, qui était basé sur Tokio 1.0 et async / await. Ce fut l'une des premières parties du moteur Workflows à être portée de Tokio 0.1 à Tokio 1.0, et l'expérience a contribué à éclairer bon nombre de nos approches et décisions architecturales au cours du processus de migration.

La bibliothèque futures qui sous-tend la plupart de l'écosystème async de Rust possède un ensemble de shims de compatibilité pour aider à migrer des futures basées sur les continuations vers des futures basées sur async / await.Cependant, ces shims ne peuvent pas prendre en compte les différences d'exécution. Notre code hérité basé sur les continuations ne pouvait être exécuté que sur le runtime Tokio 0.1, mais ce nouveau code de gestion HTTP devrait être exécuté sur le runtime Tokio 1.0, et jamais les deux ne se rencontreront : Tokio 1.0 se bloquera indéfiniment si on lui passe une future de compatibilité destinée à s'exécuter sur un exécuteur Tokio 0.1.

Par conséquent, une étape d'indirection supplémentaire était nécessaire. Au début de l'« asyncification », lors de l'utilisation d'une carte de connecteur HTTP dans Workflows, le traitement du flux se produisait initialement dans un contexte Tokio 0.1. Lors de la création de la requête HTTP, Tokio 0.1 appelait un shim, qui engendrait la requête sur un runtime Tokio 1.0 s'exécutant dans un thread séparé, et attendait que la requête soit renvoyée via un canal. De cette façon, nous pouvions éviter de mélanger les futures destinées à un runtime sur un autre. Nous adopterions plus tard cette approche à ses limites, avec chaque invocation de fonction Flow, ainsi que chaque appel à Redis passant par des shims similaires, au fur et à mesure que nous remplacions progressivement les implémentations et les bibliothèques Tokio 0.1 par des implémentations compatibles avec Tokio 1.0.

 

 

Les shims de compatibilité étaient unidirectionnels ; l'appel de code async / await à partir d'un ancien code de futures nécessitait un shim différent de l'appel d'un ancien code de futures à partir d'un code async / await. 

Pour nous assurer que les clients ne remarqueraient pas de baisse de fiabilité ou de différence de comportement, nous avons largement profité des indicateurs de fonctionnalité pour basculer entre les implémentations héritées et Tokio 1.0 des chemins de code lors de l’exécution. Si un problème était rencontré, nous serions en mesure de revenir rapidement à l’implémentation précédente pendant que nous réglions le problème avec le nouveau code. Les shims de compatibilité ont fourni un coupe-feu utile entre les deux mondes, jusqu’à ce que nous soyons prêts à le réécrire en syntaxe async / await. En exécutant à la fois les chemins hérités et Tokio 1.0 en parallèle, puis en désapprouvant le chemin de code hérité, nous avons pu remplacer progressivement la base sur laquelle le moteur Workflows était bâti jusqu’à ce qu’il ne reste plus rien de Tokio 0.1. 

Défis 

Lors du processus de migration vers Tokio 1.0, nous avons fini par créer une petite boîte à outils pour pouvoir écrire rapidement des cales de compatibilité entre le monde Tokio 0.1 et Tokio 1.0. Cela deviendrait un outil extrêmement crucial alors que nous continuions à proposer de nouvelles fonctionnalités malgré l'effort de réécriture. 

Alors que les parties principales du moteur Workflows étaient en cours de réécriture, les fonctionnalités livrées entre-temps auraient autrement dû être réécrites deux fois : une fois dans Tokio 0.1, et une autre fois dans Tokio 1.0. Le fait de disposer d'une boîte à outils pour créer des correctifs de compatibilité nous a permis d'écrire tout le nouveau code dans Tokio 1.0 et de le relier à notre base de code Tokio 0.1 existante. Parfois, les shims ne suffisaient pas ; par exemple, dans le cas du flux d’annulation susmentionné, des parties de l’implémentation devaient être écrites deux fois pour prendre en charge les chemins de code dans les deux runtimes.

Il y avait des différences fondamentales entre les runtimes hérités Tokio 0.1 et Tokio 1.0 que nous devions gérer. Avec Tokio 0.1, chaque future était allouée sur le tas, mais avec async / await, les futures seraient désormais allouées sur la pile par défaut. Si nous n'étions pas prudents, nous dépasserions rapidement la pile dans un thread de travail. En augmentant la taille de la pile de thread de travail par défaut et en choisissant soigneusement les futures à allouer sur le tas, nous avons pu atténuer ce problème.

Avec l’ancien chemin de code Tokio 0.1, plusieurs exécuteurs travaillaient en parallèle pour obtenir un rendement hautement simultané. Dans la mise en œuvre initiale avec Tokio 1.0, nous n’avons utilisé qu’un seul exécuteur pour gérer toutes les exécutions de flux. Cela s’est avéré environ 15 % plus lent que d’avoir plusieurs exécuteurs, ce qui est impressionnant en soi ! Bien que nous soyons finalement revenus à plusieurs runtimes Tokio 1.0 pour permettre le même niveau de débit que nos clients attendent, nous avons maintenant quelques nouveaux boutons à manipuler à l’avenir pour dépasser ce qui était possible avec Tokio 0.1 et l’ancien chemin de code.

Une base de code plus rapide et plus propre

Aujourd'hui, chez Workflows, nous ne dépendons plus du tout de l'ancien Tokio 0.1. Notre base de code est plus rapide, plus facile à lire et plus facile à maintenir que jamais, ce qui nous offre davantage de possibilités d'optimiser les performances d'exécution des Flow et de proposer de nouvelles fonctionnalités à nos clients plus rapidement que jamais. 

 

Graphique montrant le nombre net de lignes de code au fil du temps

 

Le passage à async / await nous a permis de supprimer plus de 20 000 lignes de code du moteur Workflows.

Nous avons enfin quitté le monde longtemps négligé de Tokio 0.1, et nous pouvons maintenant être constamment à jour avec nos dépendances fondamentales, ce qui est crucial dans nos efforts pour qu'Okta devienne l'entreprise la plus sûre au monde. Et bien sûr, il est beaucoup plus agréable pour toute notre équipe de travailler sur une base de code propre et lisible chaque jour, que sur une base de code avec des milliers de lignes de code passe-partout.

Apprenez-en davantage sur la façon d'automatiser les tâches informatiques et de sécurité critiques à grande échelle avec Okta Workflows.

Vous avez des questions concernant cet article de blog ? Contactez-nous à l'adresse eng_blogs@okta.com.

Explorez d'autres blogs d'ingénierie perspicaces d'Okta pour élargir vos connaissances.

Prêt à rejoindre notre équipe passionnée d’ingénieurs exceptionnels ? Visitez notre page carrière.

Débloquez le potentiel d'une gestion des identités moderne et sophistiquée pour votre organisation.

Contactez le service des ventes pour plus d'informations.

Continuez votre parcours dans l‘univers de l’identité