El motor que impulsa millones de ejecuciones de Okta Workflows cada día fue uno de los primeros en adoptar el ecosistema Rust asíncrono. Si bien Tokio es la opción natural para un tiempo de ejecución asíncrono en la actualidad, lanzamos Workflows antes del lanzamiento de Tokio 1.0, y mucho antes de que las prácticas recomendadas y los patrones para Rust asíncrono realmente se consolidaran.
Este artículo detalla cómo migramos el motor de Workflows, una base de código de más de 100,000 líneas de código fuente, de Tokio 0.1 basado en continuaciones a async / await Tokio 1.0, sin interrupciones del servicio ni pausa en el desarrollo de funciones.
Futuros basados en continuaciones
En los primeros tiempos, la ejecución asíncrona de Rust futures se realizaba pasando funciones que representan valores eventuales, conocidas como continuations. Estas continuations se encadenaban y cada una era ejecutada de forma asíncrona por un runtime como Tokio, y finalmente se resolvían al valor deseado. Antes de la migración, teníamos cientos de miles de líneas de código que se parecían a esto:

Este era el estado de Async Rust hasta que se estabilizó la sintaxis async / await. Para Workflows, ser uno de los primeros usuarios significó que teníamos que realizar envíos mucho antes de la estabilización de async / await, y aunque este tipo de código es difícil de leer y mantener, lo impulsamos hasta donde pudo llegar.
Nos sirvió bien, permitiéndonos hacer crecer Okta Workflows hasta donde está hoy. Avanzando rápidamente hasta hace aproximadamente un año, cuando las grietas comenzaron a volverse insoportables, y se hizo necesario finalmente comenzar la migración a la sintaxis moderna async / await y Tokio 1.0.
Futuros basados en Async / await
Con Tokio 1.0, el flujo de control asíncrono podía escribirse con la sintaxis (ahora estable desde hace mucho tiempo) async / await, lo que simplifica enormemente el código y reduce el código repetitivo. El ejemplo anterior podría escribirse así.

Incluso si no está muy familiarizado con Rust, los beneficios de legibilidad y simplicidad de usar async / await son claros. Para aquellos que están familiarizados con Rust, la capacidad de async / await para tomar prestado a través de los puntos de espera nos permitió ya no tener que pasar la propiedad del objeto Flow, por lo que ya no necesitábamos almacenarlo en FlowError. Esto fue crucial para transmitir errores a nuestros clientes y una fuente de muchos errores cuando una ruta de código inevitablemente olvida almacenar el Flow en un error. Esta sería solo una clase de errores que la migración a async / await nos ayudaría a eliminar.
El gran cambio
Si bien los beneficios de la migración a Tokio 1.0 eran claros, sería una tarea abrumadora hacerlo. La migración no es trivial y requeriría una reescritura completa del motor de Workflows. Una reescritura completa que detenga el mundo no es práctica ni está exenta de riesgos, desde el punto de vista empresarial y de la confiabilidad del cliente. No podíamos pausar el desarrollo del producto para someternos a una reescritura completa; de hecho, esto habría sido un error fatal para cualquier producto de software.
Para complicar aún más las cosas, alrededor del momento de nuestra investigación sobre la reescritura, teníamos nuevas características en preparación. Por ejemplo, enviaríamos la capacidad de cancelar un Workflow solo unos meses después, al mismo tiempo que este esfuerzo por migrar a Tokio 1.0. Una reescritura de esta magnitud tendría que hacerse poco a poco, como si se reemplazaran las ruedas de un tren en movimiento.
Capas de compatibilidad y marcas de características
Okta Workflows utiliza la biblioteca hyper HTTP para comunicarse con la web y realizar llamadas a servicios externos como Google Drive o Amazon S3, todo lo cual forma parte de lo que hace que Workflows sea tan potente. Esta biblioteca fundamental fue una de las primeras en ser reemplazada: cambiamos hyper 0.12, impulsado por Tokio 0.1, con hyper 0.14, que se basó en Tokio 1.0 y async / await. Esta fue una de las primeras partes del Workflows Engine que se portó de Tokio 0.1 a Tokio 1.0, y la experiencia ayudó a informar muchos de nuestros enfoques y decisiones arquitectónicas durante el proceso de migración.
La biblioteca de futuros que sustenta la mayor parte del ecosistema asíncrono de Rust tiene un conjunto de shims de compatibilidad para ayudar a migrar de futuros basados en continuaciones a futuros basados en async / await.Sin embargo, estos shims no pueden tener en cuenta las diferencias de tiempo de ejecución. Nuestro código heredado basado en continuaciones solo podía ejecutarse en el tiempo de ejecución de Tokio 0.1, pero este nuevo código de manejo de HTTP necesitaría ejecutarse en el tiempo de ejecución de Tokio 1.0, y nunca se encontrarán: Tokio 1.0 se colgará para siempre cuando se le pase un futuro de compatibilidad destinado a ejecutarse en un ejecutor de Tokio 0.1.
Por lo tanto, se requería un paso adicional de indirección. En los primeros días de la “asincronización”, cuando se usaba una tarjeta de conector HTTP en Workflows, el procesamiento del flujo inicialmente ocurría dentro de un contexto de Tokio 0.1. Al realizar la solicitud HTTP, Tokio 0.1 llamaría a un shim, que generaría la solicitud en un tiempo de ejecución de Tokio 1.0 que se ejecuta en un hilo separado y esperaría a que la solicitud se devolviera a través de un canal. De esta manera, podríamos evitar mezclar futuros destinados a un tiempo de ejecución en otro. Más tarde, llevaríamos este enfoque a sus límites, con cada invocación de función de Flow, así como cada llamada a Redis pasando por shims similares, a medida que reemplazamos gradualmente las implementaciones y bibliotecas de Tokio 0.1 con otras compatibles con Tokio 1.0.

Las compatibilidades eran unidireccionales; llamar al código async / await desde el código de futuros heredado requería una compatibilidad diferente que llamar al código de futuros heredado desde el código async / await.
Para ayudar a garantizar que los clientes no notaran una ca da en la fiabilidad o una diferencia en el comportamiento, aprovechamos al m ximo las marcas de caracter sticas para intercambiar entre las implementaciones heredadas y Tokio 1.0 de las rutas de c digo en tiempo de ejecuci n. Si alguna vez se encontraba un problema, podr amos cambiar a la implementaci n anterior r apidamente mientras abord amos el problema con el nuevo c digo. Las compatibilidades proporcionaron una separaci n til entre los dos mundos, hasta que estuvimos listos para reescribirlo en sintaxis async / await. Al ejecutar las rutas heredadas y Tokio 1.0 en paralelo, y luego descartar la ruta de c digo heredada, pudimos reemplazar gradualmente la base sobre la que se construy el motor de Workflows hasta que no qued nada de Tokio 0.1.
Desafíos
Durante el proceso de migración a Tokio 1.0, eventualmente construiríamos un pequeño conjunto de herramientas para poder escribir rápidamente shims de compatibilidad entre el mundo de Tokio 0.1 y Tokio 1.0. Esto se convertiría en una herramienta extremadamente crucial a medida que continuáramos enviando nuevas funciones a pesar del esfuerzo de reescritura.
Si bien las partes centrales del motor de Workflows se estaban reescribiendo, las funciones que se enviaron en el ínterin, de lo contrario, habrían tenido que reescribirse dos veces: una en Tokio 0.1 y otra en Tokio 1.0. Tener un conjunto de herramientas para crear adaptadores de compatibilidad nos permitió escribir todo el código nuevo en Tokio 1.0 y conectarlo a nuestra base de código Tokio 0.1 existente. A veces, los adaptadores no eran suficientes; por ejemplo, en el caso del flujo de cancelación antes mencionado, partes de la implementación tuvieron que escribirse dos veces para admitir rutas de código en ambos tiempos de ejecución.
Hubo algunas diferencias centrales entre los tiempos de ejecución heredados de Tokio 0.1 y Tokio 1.0 que debíamos manejar. Con Tokio 0.1, cada futuro se asignó al montón, pero con async / await, los futuros ahora se asignarían a la pila de forma predeterminada. Si no tuviéramos cuidado, rápidamente desbordaríamos la pila en un hilo de trabajo. Al aumentar el tamaño predeterminado de la pila de hilos de trabajo y elegir cuidadosamente los futuros para asignar al montón, pudimos mitigar este problema.
Con la ruta de código heredada de Tokio 0.1, varios ejecutores trabajaron en paralelo para lograr una salida altamente concurrente. En la implementación inicial con Tokio 1.0, solo usamos un único ejecutor para manejar todas las ejecuciones de flujo. ¡Esto resultó ser aproximadamente un 15% más lento que tener varios ejecutores, lo cual es impresionante en sí mismo! Si bien finalmente volvimos a tener varios tiempos de ejecución de Tokio 1.0 para permitir el mismo nivel de rendimiento que esperan nuestros clientes, ahora tenemos un par de nuevas opciones para modificar en el futuro para superar lo que era posible con Tokio 0.1 y la ruta de código heredada.
Una base de código más rápida y limpia
Hoy en Workflows, en gran medida ya no dependemos de Tokio 0.1 heredado. Nuestra base de código es más rápida, más fácil de leer y más fácil de mantener que nunca, lo que nos brinda más oportunidades para optimizar el rendimiento de la ejecución de Flow y ofrecer nuevas funciones a nuestros clientes más rápido que nunca.

La migración a async / await nos permitió eliminar más de 20,000 líneas de código del motor de Workflows.
Finalmente hemos dejado atrás el mundo largamente descuidado de Tokio 0.1 y ahora podemos estar continuamente actualizados con nuestras dependencias fundamentales, lo cual es crucial en nuestro esfuerzo por hacer que Okta se convierta en la empresa más segura del mundo. Y, por supuesto, es mucho más agradable para todo nuestro equipo trabajar en una base de código limpia y legible todos los días, que una con miles de líneas de código repetitivo.
Obtenga más información sobre cómo automatizar tareas críticas de TI y seguridad a escala con Okta Workflows.
¿Tiene preguntas sobre esta publicación de blog? Contáctenos en eng_blogs@okta.com.
Explore más Blogs de Ingeniería perspicaces de Okta para ampliar sus conocimientos.
¿Listo para unirse a nuestro apasionado equipo de ingenieros excepcionales? Visite nuestra página de carreras.
Desbloquee el potencial de la gestión de identidades moderna y sofisticada para su organización.
Póngase en contacto con Ventas para obtener más información.