Transferencia de Ray a Microsoft Windows

Por Johny vino

(Mehrdad Niknami) (28 de septiembre de 2020)

Background

Cuando Ray se lanzó por primera vez, se escribió para Linux y macOS, sistemas operativos basados ​​en UNIX. Faltaba soporte para Windows, el sistema operativo de escritorio más popular, pero era importante para el éxito a largo plazo del proyecto. Si bien el Subsistema de Windows para Linux (WSL) brindaba una opción posible para algunos usuarios, limitaba la compatibilidad con las versiones recientes de Windows 10 y dificultaba mucho más el código. para interactuar con el sistema operativo nativo de Windows, lo que equivalía a una mala experiencia para los usuarios. Teniendo esto en cuenta, realmente esperábamos proporcionar soporte nativo de Windows a los usuarios, si es posible.

Portar Ray a Windows, sin embargo, no fue una tarea trivial. Como muchos otros proyectos que intentan lograr la portabilidad a posteriori, encontramos muchos desafíos cuyas soluciones no eran obvias. En esta publicación de blog, nuestro objetivo es profundizar en los detalles técnicos del proceso que seguimos para hacer que Ray sea compatible con Windows. Esperamos que pueda ayudar a otros proyectos con una visión similar a comprender algunos de los desafíos potenciales involucrados y cómo podrían manejarse.

Descripción general

La actualización del soporte para Windows supuso un desafío cada vez mayor. desafío a medida que avanzaba el desarrollo de Ray. Antes de comenzar, esperábamos que ciertos aspectos del código constituirían una parte significativa de nuestros esfuerzos de portabilidad, incluidos los siguientes:

  • Comunicación entre procesos (IPC) y memoria compartida
  • Manejadores de objetos y manejadores / descriptores de archivos (FD)
  • Generación y administración de procesos, incluida la señalización
  • Administración de archivos, administración de subprocesos y E / S asíncronas
  • Uso de comandos del sistema y del shell
  • Redis

Dada la magnitud de los problemas, rápidamente nos dimos cuenta de que la prevención de la introducción de más incompatibilidades debe tener prioridad sobre intentar resolver cuestiones. Por lo tanto, intentamos continuar aproximadamente en los siguientes pasos, aunque esto es algo así como una simplificación, ya que a veces se abordaron problemas específicos en diferentes etapas:

  1. Compilabilidad para dependencias de terceros
  2. Compilabilidad para Ray (a través de stubs vacíos & TODOs)
  3. Vinculabilidad
  4. Integración continua (CI) (para bloquear más cambios incompatibles)
  5. Compatibilidad estática (principalmente C ++)
  6. Ejecutabilidad en tiempo de ejecución (POC mínimo)
  7. Compatibilidad en tiempo de ejecución (principalmente Python)
  8. Ejecutar -Mejoras de tiempo (p. ej., compatibilidad con Unicode)
Por Ashkan Forouzani

Proceso de desarrollo

En un alto nivel, los enfoques aquí pueden variar. Para proyectos más pequeños, puede ser muy beneficioso simplificar el código base al mismo tiempo que el código se transfiere a una nueva plataforma. Sin embargo, el enfoque que adoptamos, que resultó bastante útil para una base de código grande y que cambiaba dinámicamente, fue abordar los problemas uno a la vez , mantienen los cambios lo más ortogonales entre sí como sea posible y priorizar la preservación de la semántica sobre la simplicidad .

A veces, esto requería escribir código potencialmente extraño para manejar condiciones que pueden no haber ocurrido necesariamente en producción (digamos, citando un ruta de archivo que nunca tendría espacios). En otras ocasiones, hacerlo requería la introducción de soluciones «mecánicas» de uso más general que pueden haber sido evitables (como el uso de un std::shared_ptr para ciertos casos en los que un diseño diferente puede haber se ha permitido utilizar un std::unique_ptr). Sin embargo, preservar la semántica del código existente fue absolutamente crucial para evitar la constante introducción de nuevos errores en la base del código, lo que habría afectado al resto del equipo. Y, en retrospectiva, este enfoque fue bastante exitoso: los errores en otras plataformas resultantes de cambios relacionados con Windows fueron bastante raros y se produjeron con mayor frecuencia debido a cambios en las dependencias que a cambios semánticos en el código base.

Compilabilidad (dependencias de terceros)

El primer obstáculo fue garantizar que las dependencias de terceros pudieran construirse ellas mismas en Windows. Si bien muchas de nuestras dependencias eran bibliotecas ampliamente utilizadas y la mayoría no tenían incompatibilidades principales , esto no siempre fue sencillo. En particular, las complejidades accidentales abundan en varias facetas del problema.

  • Los archivos de compilación (en particular los archivos de compilación de Bazel) para algunos proyectos eran a veces inadecuados en Windows, requiriendo parches . A menudo, estos se debían a problemas que ocurrían con menos frecuencia en plataformas UNIX, como el problema de citando rutas de archivo con espacios, de iniciar el intérprete de Python adecuado, o el problema de vincular correctamente las bibliotecas compartidas con las bibliotecas estáticas. Afortunadamente, una de las herramientas más útiles para resolver este problema fue Bazel: su capacidad incorporada para parchear espacios de trabajo externos es, como aprendimos rápidamente, extremadamente útil. Permitió un método estándar de parchear bibliotecas externas sin modificar los procesos de compilación de una manera ad-hoc, manteniendo limpia la base de código.
  • Las cadenas de herramientas de compilación exhibieron sus propias complejidades, ya que la elección del compilador o enlazador a menudo se veía afectada las bibliotecas, API y definiciones en las que se puede confiar. Y, lamentablemente, cambiar compiladores en Bazel puede ser bastante engorroso . En Windows, en particular, la mejor experiencia a menudo viene con el uso de las herramientas de compilación de Microsoft Visual C ++, pero nuestras cadenas de herramientas en otras plataformas se basaron en GCC o Clang. Afortunadamente, la cadena de herramientas LLVM viene con Clang-Cl en Windows, lo que permite usar una combinación de funciones de Clang y MSVC, lo que hace que muchos problemas sean mucho más fáciles de resolver.
  • Las dependencias de la biblioteca generalmente presentaban la mayoría de las dificultades. A veces, el problema era tan mundano como un encabezado que faltaba o una definición conflictiva que podía manejarse mecánicamente, a la que ni siquiera las bibliotecas más utilizadas (como Boost) eran inmunes. Las soluciones a estos a menudo eran unas pocas definiciones de macro apropiadas o encabezados falsos. En otros casos, como las bibliotecas hiredis y Arrow , las bibliotecas requerían la funcionalidad POSIX que no estaba disponible en Windows (como señales, o la capacidad de pasar descriptores de archivo a través de sockets de dominio UNIX). Estos resultaron mucho más difíciles de resolver en algunos casos. Sin embargo, como estábamos más preocupados por la compilabilidad en esta etapa, fue útil posponer la implementación de API más complejas a una etapa posterior y utilizar stubs básicos o deshabilitar el código ofensivo para permitir que la compilación continúe.

Una vez que terminamos con la compilación de las dependencias, podemos concentrarnos en compilar el núcleo de Ray en sí.

Compilabilidad (Ray)

El siguiente obstáculo fue lograr que el propio Ray compilar, junto con la tienda Plasma (parte de Arrow ). Esto fue más difícil ya que el código a menudo no estaba diseñado para el modelo de Windows y se basaba en gran medida en las API POSIX. En algunos casos, esto fue solo una cuestión de encontrar y usar los encabezados apropiados (como WinSock2.h en lugar de sys/time.h para struct timeval, lo que puede resultar sorprendente) o crear sustitutos ( como

unistd.h ). En otros casos, inhabilitamos el código incompatible y dejamos TODOs para abordar en el futuro.

Si bien esto fue conceptualmente similar al caso de manejar dependencias de terceros, una preocupación única de alto nivel que surgió aquí fue minimizar los cambios en el código base , en lugar de proporcionar el solución más elegante posible. En particular, a medida que el equipo actualizaba continuamente la base de código bajo el supuesto de una API POSIX y la base de código aún no se compilaba con Windows, las soluciones que eran «integradas» y que podían emplearse de forma transparente con cambios mínimos para obtener una compatibilidad amplia todo el código base fue mucho más útil para evitar conflictos de fusión sintácticos o semánticos que las soluciones que eran de naturaleza quirúrgica. Debido a este hecho, en lugar de modificar cada sitio de llamada, creamos nuestras propias calzas de Windows equivalentes para las API POSIX que simulaban el comportamiento deseado, incluso si hacerlo no era óptimo. en general. Esto nos permitió asegurar la compilabilidad y detener la proliferación de cambios incompatibles mucho más rápidamente (y luego abordar de manera uniforme cada problema individual en todo el código base por lotes) de lo que sería posible de otra manera.

Vinculabilidad

Una vez que se logró la compilabilidad, el siguiente problema fue el enlace adecuado de los archivos objeto para obtener binarios ejecutables.Si bien teóricamente son simples, los problemas de vinculabilidad a menudo involucraban muchas complejidades accidentales, como las siguientes:

  • Algunas bibliotecas del sistema eran específicas de la plataforma y no estaban disponibles o eran diferentes en otras plataformas ( como libpthread)
  • Ciertas bibliotecas se vincularon dinámicamente cuando se esperaba que estuvieran vinculadas estáticamente (o viceversa)
  • Algunas definiciones de símbolos faltaban o estaban en conflicto (por ejemplo, connect de hiredis en conflicto con connect para sockets)
  • Ciertas dependencias funcionaban casualmente en sistemas POSIX pero requerían un manejo explícito en Bazel en Windows

Las soluciones para estos a menudo eran cambios mundanos (aunque quizás no obvios) en los archivos de compilación, aunque en algunos casos, era necesario parchear las dependencias. Como antes, la capacidad de Bazel para parchear prácticamente cualquier archivo de una fuente externa fue extremadamente útil para ayudar a resolver estos problemas.

Por Richy Great

Conseguir que la base de código se compile correctamente fue un hito importante. Una vez integrados los cambios específicos de la plataforma, todo el equipo asumiría la responsabilidad de garantizar la portabilidad estática en todos los cambios futuros y la introducción de cambios incompatibles se minimizaría en todo el código. (La portabilidad dinámica , por supuesto, todavía no era posible en este punto, ya que los stubs a menudo carecían de la funcionalidad necesaria en Windows y, en consecuencia, el código no se podía ejecutar en esta etapa).

Lograr que la base de código se compile correctamente fue un hito importante. Una vez integrados los cambios específicos de la plataforma, todo el equipo asumiría la responsabilidad de garantizar la portabilidad estática en todos los cambios futuros y la introducción de cambios incompatibles se minimizaría en todo el código. (La portabilidad dinámica , por supuesto, todavía no era posible en este punto, ya que los stubs a menudo carecían de la funcionalidad necesaria en Windows y, en consecuencia, el código no se podía ejecutar en esta etapa).

Esto no solo disminuyó la carga de trabajo, sino que también permitió que el esfuerzo de portabilidad de Windows se ralentizara y procediera de manera más lenta y cuidadosa para abordar incompatibilidades más difíciles en profundidad, sin necesidad de desviar la atención a los cambios importantes que se introducen constantemente en la base de código. Esto permitió soluciones de mayor calidad a largo plazo, incluida una refactorización del código base que potencialmente afectó la semántica en otras plataformas.

Compatibilidad estática (principalmente C / C ++)

Esto fue posiblemente la etapa más lenta, en la que se eliminarían o elaborarían códigos auxiliares de compatibilidad, se volvería a habilitar el código deshabilitado y se abordarían problemas individuales. La naturaleza estática de C ++ permitió que los diagnósticos del compilador guiaran la mayoría de estos cambios necesarios sin la necesidad de ejecutar ningún código.

Los cambios individuales aquí, incluidos algunos mencionados anteriormente, serían demasiado largos para discutirlos en profundidad. en su totalidad, y ciertos problemas individuales probablemente serían dignos de sus propias publicaciones de blog extensas. El proceso de generación y gestión, por ejemplo, es en realidad un esfuerzo sorprendentemente complejo lleno de idiosincrasias y trampas (consulte aquí , por ejemplo) para el que parece no haber nada bueno, biblioteca multiplataforma, a pesar de ser un problema antiguo. Parte de esto se debe a los diseños iniciales deficientes en las API del sistema operativo. ( El código fuente de Chromium ofrece un vistazo a algunas de las complejidades que a menudo se ignoran en otros lugares. De hecho, el código fuente de Chromium a menudo puede servir como una excelente ilustración de cómo manejar muchas incompatibilidades y sutilezas de plataforma.) Nuestro desprecio inicial por este hecho nos llevó a intentar utilizar Boost.Process, pero la falta de una semántica de propiedad clara para pid_t de POSIX (que se usa para ambos identificación y propiedad del proceso), así como errores en la biblioteca de Boost.Process en sí resultó en la dificultad de encontrar errores en la base de código, lo que finalmente resultó en nuestra decisión de revertir este cambio e introducir nuestra propia abstracción. Además, la biblioteca Boost.Process era bastante pesada incluso para una biblioteca Boost, y esto ralentizó nuestras compilaciones considerablemente. En su lugar, terminamos escribiendo nuestro propio contenedor para los objetos de proceso. Esto resultó funcionar bastante bien para nuestros propósitos. Una de nuestras conclusiones en este caso fue considerar la posibilidad de adaptar las soluciones a nuestras propias necesidades y no asumir que las soluciones preexistentes son la mejor opción .

Esto fue, por supuesto, solo un vistazo al tema de la gestión de procesos.Quizás también valga la pena profundizar en algunos otros aspectos del esfuerzo de portabilidad.

Por Thomas Jensen

Partes del código base (como la comunicación con el servidor Plasma en Arrow) asumidas la capacidad de utilizar sockets de dominio UNIX (AF_UNIX) en Windows. Si bien las versiones recientes de Windows 10 admiten sockets de dominio UNIX, las implementaciones tampoco son suficientes para cubrir todos los usos posibles del dominio UNIX sockets, ni los sockets de dominio UNIX son particularmente elegantes: requieren limpieza manual y pueden dejar archivos innecesarios en el sistema de archivos en el caso de una terminación no limpia de un proceso, y no brindan la capacidad de enviar datos auxiliares (como descriptores de archivo) a otro proceso. Como Ray usa Boost.Asio, el reemplazo más conveniente para los sockets de dominio UNIX fueron los sockets TCP locales (los cuales podrían abstraerse como sockets generales), por lo que cambiamos al último, simplemente reemplazar rutas de archivo con versiones en cadena de direcciones TCP / IP sin necesidad de refactorizar el código base.

Esto no fue suficiente, ya que el uso de sockets TCP aún no brindaba la capacidad de replicar descriptores de socket en otros procesos. De hecho, una solución adecuada a este problema habría sido evitarlo por completo. Como eso requeriría una refactorización a más largo plazo de las partes relevantes del código base (una tarea en la que otros se embarcaron), sin embargo, un enfoque transparente pareció más apropiado en el ínterin. Esto se hizo difícil por el hecho de que, en sistemas basados ​​en UNIX, la duplicación de un descriptor de archivo no requiere el conocimiento de la identidad del proceso de destino, mientras que, en Windows, la duplicación de un identificador requiere la manipulación activa proceso de destino. Para solucionar estos problemas, implementamos la capacidad de intercambiar descriptores de archivos reemplazando el procedimiento de protocolo de enlace al iniciar la tienda Plasma por un mecanismo más especializado que establecería una conexión TCP. , busque el ID del proceso en el otro extremo (un procedimiento quizás lento, pero único), duplique el identificador del socket e informe al proceso de destino del nuevo identificador. Si bien esta no es una solución de propósito general (y de hecho puede ser propensa a las condiciones de carrera en el caso general) y es un enfoque bastante inusual, funcionó bien para los propósitos de Ray, y puede ser un enfoque para otros proyectos que enfrentan el mismo problema. podría beneficiarse.

Más allá de esto, uno de los mayores problemas que esperábamos enfrentar era nuestra dependencia del servidor Redis . Si bien Microsoft Open Technologies (MSOpenTech) había implementado previamente un puerto de Redis para Windows, el proyecto se había abandonado y, en consecuencia, no admitía las versiones de Redis que Ray requería. . Esto inicialmente nos hizo suponer que aún necesitaríamos ejecutar el servidor Redis en el Subsistema de Windows para Linux (WSL), lo que probablemente habría resultado inconveniente para los usuarios. Entonces, nos sentimos muy agradecidos de descubrir que otro desarrollador había continuado con el proyecto para producir binarios de Redis posteriores en Windows (consulte tporadowski / redis ). Esto simplificó enormemente nuestro problema y nos permitió brindar soporte nativo de Ray para Windows.

Finalmente, posiblemente los obstáculos más importantes que enfrentamos (al igual que MSOpenTech Redis y la mayoría de los otros programas solo para POSIX) fueron la ausencia de sustitutos triviales para algunas API POSIX en Windows. Algunos de estos ( como

getppid()) eran sencillos, aunque algo tediosos. Sin embargo, posiblemente el problema más difícil encontrado durante todo el proceso de transferencia fue el de los descriptores de archivos frente a los identificadores de archivos. Gran parte del código en el que confiamos (como la tienda Plasma en Arrow) asumió el uso de descriptores de archivos POSIX (int s). Sin embargo, Windows usa de forma nativa HANDLE s, que tienen el tamaño de un puntero y son análogos a size_t. Sin embargo, en sí mismo, esto no es un problema significativo, ya que el tiempo de ejecución de Microsoft Visual C ++ (CRT) proporciona una capa similar a POSIX. Sin embargo, la capa tiene una funcionalidad limitada, requiere traducciones en todos los sitios de llamadas que no la admiten y, en particular, no se puede utilizar para cosas como sockets o identificadores de memoria compartida. Además, no queríamos asumir que HANDLE s siempre sería lo suficientemente pequeño como para caber en un entero de 32 bits, aunque este era a menudo el caso, ya que no estaba claro si circunstancias de las que no éramos conscientes podrían romper esta suposición en silencio.Esto agravó significativamente nuestros problemas, ya que la solución más obvia habría sido detectar todos los int que representan descriptores de archivos en una biblioteca como Arrow, y reemplazarlos (y todos sus usos ) con un tipo de datos alternativo, que era un proceso propenso a errores e involucraba parches significativos en el código externo, creando una carga de mantenimiento significativa.

Fue bastante difícil decidir qué hacer en esta etapa. La solución de MSOpenTech Redis para el mismo problema dejó en claro que se trataba de una tarea bastante abrumadora, ya que habían resuelto este problema creando una tabla de descriptor de archivos singleton para todo el proceso en la parte superior de la implementación CRT existente, requiriéndoles que se ocupen de la seguridad de los subprocesos, además de obligarlos a interceptar todos usos de las API POSIX (incluso aquellos que ya era capaz de manejar) simplemente para traducir descriptores de archivos. En su lugar, decidimos adoptar un enfoque inusual: ampliamos la capa de traducción POSIX en el CRT . Esto se hizo identificando identificadores incompatibles en el momento de la creación y «empujando» esos identificadores en los búferes de una tubería superflua, devolviendo el descriptor de esa tubería en su lugar. Luego, solo tuvimos que modificar los sitios de uso de estos identificadores, que fueron, de manera crucial, triviales de identificar, ya que todos eran socket y archivo mapeado en memoria . De hecho, esto ayudó a evitar la necesidad de parches, ya que pudimos simplemente redirigir muchas funciones a través de macros .

Si bien el esfuerzo de desarrollar esta capa de extensión única (en win32fd.h) fue significativa (y bastante poco ortodoxa), probablemente valió la pena, ya que la capa de traducción era de hecho bastante pequeño en comparación y nos permitió delegar la mayoría de los problemas no relacionados (como el bloqueo multiproceso de la tabla de descriptores de archivos) a las API de CRT. Además, al aprovechando las canalizaciones anónimas de la misma tabla de descriptor de archivos global (a pesar de nuestra falta de acceso directo a ella), pudimos evitar tener que interceptar y traducir descriptores de archivo para otras funciones que ya podrían manejarse directamente. Esto permitió que gran parte del código permaneciera esencialmente sin cambios con un impacto mínimo en el rendimiento, hasta que más tarde tuvimos la oportunidad de refactorizar el código y proporcionar mejores envoltorios en un nivel superior (como a través de los envoltorios Boost.Asio). Es muy posible que una extensión de esta capa permita que otros proyectos, como Redis, se migren a Windows de manera mucho más fluida y con cambios mucho menos drásticos o posibles errores.

Por Andrea Leopardi

Ejecutabilidad en tiempo de ejecución (prueba de concepto)

Una vez que creímos que el núcleo de Ray estaba funcionando correctamente, el siguiente hito fue garantizar que una prueba de Python pudiera ejercitar con éxito nuestras rutas de código. Inicialmente, no priorizamos hacerlo. Sin embargo, esto resultó ser un error, ya que los cambios posteriores de otros desarrolladores del equipo de hecho introdujeron incompatibilidades más dinámicas con Windows y el sistema CI no pudo detectar tales roturas. Por lo tanto, posteriormente lo convertimos en una prioridad para ejecutar una prueba mínima en las compilaciones de Windows, para evitar más roturas de la compilación.

Para el En su mayor parte, nuestros esfuerzos fueron exitosos, y cualquier error persistente en el núcleo de Ray se encontraba en partes predecibles del código base (aunque abordarlos a menudo requería pasar por código multiproceso, que estaba lejos de ser trivial). Sin embargo, hubo al menos una sorpresa algo desagradable en el camino en el lado C y otra en el lado de Python, las cuales (entre otras cosas) nos animaron a leer la documentación del software de manera más proactiva en el futuro.

En el lado C, nuestro contenedor inicial de protocolo de enlace para intercambiar identificadores de socket se basó en reemplazar ingenuamente sendmsg y recvmsg con WSASendMsg y WSARecvMsg. Estas API de Windows eran los equivalentes más cercanos de las API POSIX y, por lo tanto, parecían ser una opción obvia. Sin embargo, tras la ejecución, el código se bloqueaba constantemente y el origen del problema no estaba claro. Algunas depuraciones (incluidas las versiones de depuración de los tiempos de ejecución &) ayudaron a revelar que el problema estaba en las variables de pila pasadas a WSASendMsg. Una mayor depuración y una inspección más detallada del contenido de la memoria sugirieron que el problema podría haber sido el campo msg_flags de WSAMSG, ya que este era el único sin inicializar campo.Sin embargo, esto pareció ser irrelevante: msg_flags simplemente se tradujo de flags en struct msghdr, que no se usó en la entrada y simplemente se usó como un parámetro de salida. Sin embargo, la lectura de la documentación reveló el problema: en Windows, el campo también servía como parámetro de entrada y, por lo tanto, dejarlo sin inicializar resultó en un comportamiento impredecible. Esto fue bastante inesperado para nosotros, y resultó en dos conclusiones importantes en el futuro: leer la documentación de cada función cuidadosamente , y Además, que la inicialización de variables no se trata simplemente de garantizar la corrección con las API actuales, sino también importante para hacer que el código sea robusto para cambios futuros en las API de destino .

En el lado de Python, encontramos un problema diferente. Nuestros módulos nativos de Python inicialmente no se cargarían, a pesar de la falta de problemas obvios. Después de varios días de conjeturas, revisando el ensamblaje y el código fuente de CPython e inspeccionando las variables en la base de código de CPython, se hizo evidente que el problema era la falta de un sufijo .pyd en Python dinámico. bibliotecas en Windows. Resulta que, por razones que no nos quedan claras, Python se niega a cargar incluso .dll archivos en Windows como módulos de Python, a pesar de que las bibliotecas compartidas nativas normalmente podrían cargarse incluso con cualquier archivo extensión. De hecho, resultó que este hecho estaba documentado en el sitio web de Python . Lamentablemente, sin embargo, la presencia de dicha documentación no podría implicar que nos demos cuenta de buscarla.

Sin embargo, finalmente, Ray pudo ejecutarse con éxito en Windows, y esto concluyó el siguiente hito y proporcionó una prueba del concepto para el esfuerzo.

Por Hitesh Choudhary

Compatibilidad en tiempo de ejecución (principalmente Python)

En este punto, una vez que el núcleo de Ray estaba trabajando, pudimos concentrarnos en portar código de nivel superior. Algunos problemas fueron bastante fáciles de abordar, por ejemplo, algunas API de Python que son solo para UNIX (por ejemplo, os.uname()[1]) a menudo tienen reemplazos adecuados en Windows (como socket.gethostname()), y encontrarlos era una cuestión de saber buscar todas las instancias de ellos en el código base. Otros problemas fueron más difíciles de localizar o resolver. A veces, se debían al uso de comandos específicos de POSIX (como ps), que requerían enfoques alternativos (como el uso de psutil para Python). Otras veces, se debieron a incompatibilidades en bibliotecas de terceros. Por ejemplo, cuando un socket se desconecta en Windows, se genera un error, en lugar de resultar en lecturas vacías. La biblioteca de Python para Redis no pareció manejar esto. Tales diferencias en el comportamiento requerían parches de mono explícitos para evitar errores confusos ocasionales que ocurrirían al finalizar Ray.

Si bien algunos de estos problemas son bastante tedioso pero probablemente esperado (como reemplazar los usos de /tmp con el directorio temporal de la plataforma, o evitar la suposición de que todas las rutas absolutas comienzan con una barra), algunas fueron algo inesperadas (como reservas de puertos en conflicto) o (como suele ser el caso) debido a suposiciones erróneas, y comprenderlas depende de comprender la arquitectura de Windows y sus enfoques de compatibilidad con versiones anteriores.

Una de esas historias gira en torno al uso de barras inclinadas como separadores de directorios en Windows. En general, estos parecen funcionar bien y los desarrolladores los usan comúnmente. Sin embargo, esto se debe a la conversión automática de barras inclinadas a barras invertidas en las bibliotecas del subsistema de Windows en modo de usuario, y se puede suprimir cierto procesamiento automático prefijando explícitamente las rutas con un prefijo \\?\, que es útil para omitir ciertas características de compatibilidad (como rutas largas). Sin embargo, nunca usamos explícitamente esa ruta, y asumimos que se podía esperar que los usuarios evitaran el uso inusual en nuestras versiones experimentales. Sin embargo, más tarde se hizo evidente que cuando Bazel invocó ciertas pruebas de Python, las rutas se procesarían en este formato para permitir el uso de rutas largas, y esto deshabilitó la traducción automática en la que confiamos implícitamente. Esto nos llevó a conclusiones importantes: en primer lugar, generalmente es mejor usar las API de la manera más adecuada para el sistema de destino, ya que brinda la menor cantidad de oportunidades para que ocurran problemas inesperados. .En segundo lugar, y lo más importante, es simplemente una falacia suponer que el entorno de un usuario es predecible . La realidad es que el software moderno casi siempre se basa en ejecutar código de terceros cuyos comportamientos precisos desconocemos. Incluso cuando se puede suponer que un usuario evita situaciones problemáticas, el software de terceros desconoce por completo tales suposiciones ocultas. Por lo tanto, es probable que ocurran de todos modos, no solo para los usuarios, sino también para los propios desarrolladores de software, lo que da como resultado errores que son más difíciles de rastrear que de corregir al escribir el código inicial. Por lo tanto, es importante evitar poner demasiado peso en la facilidad de uso del programa (lo opuesto a la «facilidad de uso») al diseñar un sistema robusto.

(Aparte de la diversión: de hecho, en Windows, las rutas De hecho, puede contener comillas y muchos otros caracteres especiales que normalmente se supone que son ilegales. Esto ocurre cuando se utilizan flujos de datos alternativos NTFS. Sin embargo, estos son raros y lo suficientemente complejos como para que incluso las bibliotecas de lenguaje estándar a menudo no los manejen).

Una vez que se resolvieron los problemas más importantes, sin embargo, muchas pruebas pudieron pasar a Windows, creando la primera implementación experimental de Ray en Windows.

Por Ross Sneddon

Mejoras en el tiempo de ejecución (por ejemplo, compatibilidad con Unicode)

En este punto, gran parte del núcleo de Ray se puede usar en Windows al igual que en otras plataformas. No obstante, quedan algunos problemas que requieren esfuerzos continuos para solucionarlos.

El soporte Unicode es uno de esos problemas. Debido a razones históricas, el subsistema de modo de usuario de Windows tiene dos versiones de la mayoría de las API: una versión «ANSI» que admite juegos de caracteres de un solo byte y una versión «Unicode» que admite UCS-2 o UTF-16 (según la datos de la API en cuestión). Desafortunadamente, ninguno de estos es UTF-8; incluso el soporte básico para Unicode requiere el uso de cadenas de caracteres anchos (basadas en wchar_t) en todo el código base. (nb: De hecho, Microsoft ha intentado recientemente introducir UTF-8 como una página de códigos, pero no cuenta con el soporte suficiente para abordar este problema sin problemas, al menos sin depender de componentes internos de Windows potencialmente indocumentados y frágiles).

Tradicionalmente, los programas de Windows manejan Unicode mediante el uso de macros como _T() o TEXT(), que se expanden a estrecho o ancho -caracteres literales dependiendo de si se especifica una compilación Unicode, y usan TCHAR como sus tipos de caracteres genéricos. Del mismo modo, la mayoría de las API de C tienen versiones TCHAR dependientes (como _tcslen() en lugar de strlen()) para permitir la compatibilidad con ambos tipos de código. Sin embargo, la migración de una base de código basada en UNIX a este modelo es un esfuerzo bastante complicado. Esto aún no se ha hecho en Ray y, por lo tanto, al momento de escribir este artículo, Ray no es compatible con Unicode adecuado en (por ejemplo) rutas de archivo en Windows, y el mejor enfoque para hacerlo aún puede ser una pregunta abierta.

Otro problema similar es el mecanismo de comunicación entre procesos. Si bien los sockets TCP pueden funcionar bien en Windows, son subóptimos, ya que introducen una capa de complejidad innecesaria en la lógica (como tiempos de espera, mantenimiento activo, el algoritmo de Nagle), pueden resultar en accesibilidad accidental desde hosts no locales y pueden introducir algunos gastos generales de rendimiento. En el futuro, Named Pipes podría proporcionar un mejor reemplazo para los sockets de dominio UNIX en Windows; de hecho, incluso en Linux, las tuberías o los llamados abstractos sockets de dominio UNIX también pueden resultar mejores alternativas, ya que no requieren desorden y limpieza de archivos de sockets en el sistema de archivos.

Finalmente, otro ejemplo de tal problema es la compatibilidad de sockets BSD, o mejor dicho, la falta de ella. Una excelente respuesta sobre StackOverflow analiza algunos de los problemas en profundidad, pero brevemente, mientras que las API de socket comunes son derivadas de la API de socket BSD original, las diferentes plataformas implementan un socket similar banderas de manera diferente. En particular, los conflictos con las direcciones IP o los puertos TCP existentes pueden producir comportamientos diferentes entre plataformas. Si bien los problemas son difíciles de detallar aquí, el resultado final es que esto puede dificultar el uso de múltiples instancias de Ray en el mismo host simultáneamente. (De hecho, como esto depende del comportamiento del kernel del sistema operativo, también afecta a WSL). Este es otro problema conocido cuya solución es bastante complicada y no se aborda por completo en el sistema actual.

Conclusión

El proceso de migrar una base de código como la de Ray a Windows ha sido una experiencia valiosa que destaca los pros y los contras de muchos aspectos del desarrollo de software y su impacto en el mantenimiento del código.La descripción anterior solo destaca algunos de los obstáculos encontrados en el camino. Se pueden extraer muchas conclusiones útiles del proceso, algunas de las cuales pueden ser valiosas para compartir aquí para otros proyectos con la esperanza de lograr un objetivo similar.

Primero, en algunos casos, descubrimos más tarde que versiones posteriores de algunas bibliotecas (como hiredis) ya habían resuelto algunos problemas que habíamos abordado. Las soluciones no siempre fueron obvias, ya que (por ejemplo) la versión de hiredis dentro de las versiones recientes de Redis era en realidad una copia obsoleta de hiredis, lo que nos llevó a creer que algunos problemas aún no se habían abordado. Las revisiones posteriores tampoco siempre abordaron por completo todos los problemas de compatibilidad existentes. Sin embargo, posiblemente habría ahorrado un poco de esfuerzo buscar más a fondo las soluciones existentes para algunos problemas para evitar tener que resolverlos nuevamente.

Por John Barkiple

Segundo, la cadena de suministro de software suele ser compleja . Los errores pueden agravarse de forma natural en cada capa, y es una falacia confiar en que incluso las herramientas de código abierto más utilizadas serán «probadas en batalla» y, por lo tanto, robustas, especialmente cuando se utilizan en ira . Además, muchos problemas de ingeniería de software comunes o de larga data no tienen soluciones satisfactorias disponibles para su uso, especialmente (pero no solo) cuando requieren compatibilidad entre diferentes sistemas. De hecho, aparte de meras peculiaridades, en el proceso de migración de Ray a Windows, encontramos y a menudo informamos errores en numerosas piezas de software, incluido, entre otros, un error de Git en Linux que afectó el uso de Bazel, Redis (Linux) , glog , psutil (error de análisis que afecta a WSL) , grpc , muchos errores difíciles de identificar en el propio Bazel (p. ej., 1 , 2 , 3 , 4 ), Travis CI y Acciones de GitHub , entre otras. Esto también nos animó a prestar más atención a las complejidades de nuestras dependencias.

En tercer lugar, invertir en herramientas e infraestructura paga dividendos a largo plazo. Las compilaciones más rápidas permiten un desarrollo más rápido y las herramientas más poderosas permiten resolver problemas complejos con mayor facilidad. En nuestro caso, el uso de Bazel nos ayudó de muchas maneras, a pesar de estar lejos de ser perfecto y su imposición de una curva de aprendizaje pronunciada. Invertir algo de tiempo (posiblemente varios días) para aprender las capacidades, fortalezas y deficiencias de una nueva herramienta rara vez es fácil, pero probablemente sea beneficioso para el mantenimiento del código. En nuestro caso, dedicar un tiempo a leer la documentación de Bazel en profundidad nos permitió identificar mucho más rápidamente una multitud de problemas y soluciones futuros. Además, también nos ayudó a integrar herramientas con Bazel que aparentemente pocos otros habían logrado, como la herramienta include-what-you-use de Clang.

En cuarto lugar, y como se mencionó anteriormente, es prudente participar en prácticas de codificación seguras como inicializar la memoria antes de su uso cuando no hay una compensación significativa. Incluso el ingeniero más cuidadoso no puede necesariamente predecir la evolución futura del sistema subyacente que puede invalidar silenciosamente las suposiciones.

Por último, como es generalmente el caso en medicina, prevención es la mejor cura . Tener en cuenta los posibles desarrollos futuros y la codificación para interfaces estandarizadas permite un diseño de código más extensible que el que se puede lograr fácilmente después de que surjan incompatibilidades.

Si bien la adaptación de Ray a Windows aún no está completa, ha sido bastante hasta ahora exitoso, y esperamos que compartir nuestra experiencia y soluciones pueda servir como una guía útil para otros desarrolladores que estén considerando embarcarse en un viaje similar.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *