Portage de Ray vers Microsoft Windows

Par Johny vino

(Mehrdad Niknami) (28 septembre 2020)

Background

Lorsque Ray a été lancé pour la première fois, il a été écrit pour Linux et macOS – systèmes dexploitation basés sur UNIX. La prise en charge de Windows, le système dexploitation de bureau le plus populaire, faisait défaut, mais était importante pour le succès à long terme du projet. Alors que le sous-système Windows pour Linux (WSL) offrait une option possible à certains utilisateurs, il limitait la prise en charge des versions récentes de Windows 10 et rendait beaucoup plus difficile le code dinteragir avec le système dexploitation Windows natif, ce qui représentait une mauvaise expérience pour les utilisateurs. Compte tenu de cela, nous espérions vraiment fournir une prise en charge native de Windows aux utilisateurs, si possible.

Le portage de Ray vers Windows, cependant, était loin dêtre une tâche triviale. Comme beaucoup dautres projets qui tentent de parvenir à la portabilité après coup, nous avons rencontré de nombreux défis dont les solutions nétaient pas évidentes. Dans cet article de blog, nous visons à approfondir les détails techniques du processus que nous avons suivi pour rendre Ray compatible avec Windows. Nous espérons que cela pourra aider dautres projets ayant une vision similaire à comprendre certains des défis potentiels impliqués et comment ils pourraient être traités.

Présentation

La prise en charge de la modernisation de Windows posait un problème de plus en plus important défi au fur et à mesure que le développement sur Ray progressait. Avant de commencer, nous nous attendions à ce que certains aspects du code constituent une partie importante de nos efforts de portabilité, notamment les suivants:

  • Communication inter-processus (IPC) et mémoire partagée
  • Poignées dobjets et descripteurs / descripteurs de fichiers (FD)
  • Création et gestion de processus, y compris la signalisation
  • Gestion de fichiers, gestion des threads et E / S asynchrones
  • Utilisation du shell et des commandes système
  • Redis

Compte tenu de lampleur des problèmes, nous nous sommes rapidement rendu compte quil fallait donner la priorité à la prévention de nouvelles incompatibilités par rapport aux tentatives de résolution des problèmes existants. problèmes. Nous avons donc tenté de procéder à peu près aux étapes suivantes, bien quil sagisse en quelque sorte dune simplification car des problèmes spécifiques ont parfois été traités à différentes étapes:

  1. Compilabilité pour les dépendances tierces
  2. Compilabilité pour Ray (via des stubs vides & TODOs)
  3. Linkability
  4. Intégration continue (CI) (pour bloquer dautres modifications incompatibles)
  5. Compatibilité statique (principalement C ++)
  6. Exécutabilité dexécution (POC minimal)
  7. Compatibilité dexécution (principalement Python)
  8. Exécution -time améliorations (par exemple, prise en charge Unicode)
Par Ashkan Forouzani

Processus de développement

À un niveau élevé, les approches ici peuvent varier. Pour les petits projets, il peut être très avantageux de simplifier la base de code en même temps que le code est porté sur une nouvelle plate-forme. Cependant, l’approche que nous avons adoptée, qui s’est avérée très utile pour une base de code volumineuse et changeante de manière dynamique, consistait à résoudre les problèmes un par un , conserver les modifications aussi orthogonales que possible entre elles et donner la priorité à la préservation de la sémantique par rapport à la simplicité .

Parfois, cela nécessitait décrire du code potentiellement étranger pour gérer des conditions qui ne se seraient pas nécessairement produites en production (par exemple, en citant un chemin daccès au fichier qui naurait jamais despace). À dautres moments, cela nécessitait lintroduction de solutions «mécaniques» plus générales qui auraient pu être évitées (comme lutilisation dun std::shared_ptr dans certains cas où une conception différente peut avoir a été autorisé à utiliser un std::unique_ptr). Cependant, la préservation de la sémantique du code existant était absolument cruciale pour éviter lintroduction constante de nouveaux bogues dans la base de code, ce qui aurait affecté le reste de léquipe. Et, avec le recul, cette approche a été assez réussie: les bogues sur dautres plates-formes résultant de modifications liées à Windows étaient assez rares, et se produisaient le plus souvent en raison de changements dans les dépendances que de changements sémantiques dans la base de code.

Compilabilité (dépendances tierces)

Le premier obstacle était de sassurer que les dépendances tierces pouvaient elles-mêmes être construites sur Windows. Alors que beaucoup de nos dépendances étaient des bibliothèques largement utilisées et que la plupart navaient pas dincompatibilités majeures , ce nétait pas toujours simple. En particulier, les complexités accidentelles abondaient dans plusieurs facettes du problème.

  • Les fichiers de construction (en particulier les fichiers de construction Bazel) pour certains projets étaient parfois inadéquats sous Windows, nécessitant des correctifs . Ces problèmes étaient souvent dus à des problèmes qui se produisaient plus rarement sur les plates-formes UNIX, tels que le problème de citant les chemins de fichiers avec des espaces, de lancer linterpréteur Python approprié, ou le problème de relier correctement les bibliothèques partagées et les bibliothèques statiques. Heureusement, lun des outils les plus utiles pour résoudre ce problème était Bazel lui-même: sa capacité intégrée à patcher les espaces de travail externes est, comme nous lavons rapidement appris, extrêmement utile. Il permettait une méthode standard de correction des bibliothèques externes sans modifier les processus de construction de manière ad hoc, en gardant la base de code propre.
  • Les chaînes doutils de construction présentaient leurs propres complexités, car le choix du compilateur ou de léditeur de liens affectait souvent les bibliothèques, API et définitions sur lesquelles on peut sappuyer. Et malheureusement, changer de compilateur sur Bazel peut être assez encombrant . Sur Windows, en particulier, la meilleure expérience consiste souvent à utiliser les outils de construction Microsoft Visual C ++, mais nos chaînes doutils sur dautres plates-formes étaient basées sur GCC ou Clang. Heureusement, la chaîne doutils LLVM est livrée avec Clang-Cl sous Windows, ce qui permet dutiliser un mélange de fonctionnalités Clang et MSVC, ce qui rend de nombreux problèmes beaucoup plus faciles à résoudre.
  • Les dépendances de bibliothèque présentaient généralement le plus de difficultés. Parfois, le problème était aussi banal quun en-tête manquant ou une définition contradictoire pouvant être gérée mécaniquement, ce à quoi même les bibliothèques largement utilisées (telles que Boost) nétaient pas à labri. Les solutions à ces problèmes étaient souvent quelques définitions de macro ou en-têtes factices appropriés. Dans dautres cas, comme pour les bibliothèques hiredis et Arrow , les bibliothèques nécessitaient une fonctionnalité POSIX qui nétait pas disponible sur Windows (comme les signaux, ou la possibilité de transmettre des descripteurs de fichier sur les sockets du domaine UNIX). Ces problèmes se sont avérés beaucoup plus difficiles à résoudre dans certains cas. Cependant, comme nous étions plus préoccupés par la compilabilité à ce stade, il était utile de reporter la mise en œuvre dAPI plus complexes à une étape ultérieure et dutiliser des stubs de base ou de désactiver le code incriminé pour permettre à la construction de se poursuivre.

Une fois que nous aurions fini de compiler les dépendances, nous pourrions nous concentrer sur la compilation du cœur de Ray lui-même.

Compilabilité (Ray)

Le prochain obstacle était de faire en sorte que Ray lui-même compilez, ainsi que le magasin Plasma (qui fait partie de la Flèche ). Cela a été plus difficile car le code nétait souvent pas du tout conçu pour le modèle Windows et reposait fortement sur les API POSIX. Dans certains cas, il sagissait simplement de trouver et dutiliser les en-têtes appropriés (tels que WinSock2.h au lieu de sys/time.h pour struct timeval, ce qui peut surprendre) ou en créant des substituts ( comme pour

unistd.h ). Dans dautres cas, nous avons désactivé le code incompatible et laissé les TODOs à traiter à lavenir.

Même si cela était conceptuellement similaire au cas de gestion des dépendances tierces, une préoccupation de haut niveau unique qui a émergé ici était de minimiser les modifications apportées à la base de code , au lieu de solution la plus élégante possible. En particulier, alors que léquipe mettait continuellement à jour la base de code sous lhypothèse dune API POSIX et que la base de code ne se compilait pas encore avec Windows, des solutions qui étaient «drop-in» et qui pouvaient être utilisées de manière transparente avec des changements minimes pour obtenir une compatibilité globale la base de code entière était beaucoup plus utile pour éviter les conflits de fusion syntaxique ou sémantique que les solutions de nature chirurgicale. De ce fait, au lieu de modifier chaque site dappel, nous avons créé notre propre équivalent Windows shims pour les API POSIX qui simulaient le comportement souhaité, même si cela était sous-optimal global. Cela nous a permis de garantir la compilabilité et darrêter la prolifération de modifications incompatibles beaucoup plus rapidement (et de traiter plus tard de manière uniforme chaque problème individuel sur lensemble de la base de code en batch) que cela ne serait possible autrement.

Linkability

Une fois la compilabilité atteinte, le problème suivant était la liaison appropriée des fichiers objets pour obtenir des binaires exécutables.Bien que théoriquement simples, les problèmes de liaison impliquaient souvent de nombreuses complexités accidentelles, telles que les suivantes:

  • Certaines bibliothèques système étaient spécifiques à la plate-forme et indisponibles ou différentes sur dautres plates-formes ( comme libpthread)
  • Certaines bibliothèques étaient liées dynamiquement alors quelles étaient censées lêtre statiquement (ou vice-versa)
  • Certaines définitions de symboles étaient manquantes ou en conflit (par exemple, connect de hiredis en conflit avec connect pour les sockets)
  • Certaines dépendances fonctionnaient par coïncidence sur les systèmes POSIX mais nécessitaient un traitement explicite dans Bazel sous Windows

Les solutions à ces problèmes étaient souvent des modifications banales (bien que peut-être non évidentes) des fichiers de construction, bien que dans certains cas, il était nécessaire de corriger les dépendances. Comme auparavant, la capacité de Bazel à corriger pratiquement tous les fichiers dune source externe a été extrêmement utile pour résoudre ces problèmes.

Par Richy Great

La compilation de la base de code était une étape majeure. Une fois les modifications spécifiques à la plate-forme intégrées, toute léquipe assumerait désormais la responsabilité dassurer la portabilité statique de toutes les modifications futures et lintroduction de modifications incompatibles serait minimisée dans lensemble de la base de code. (La portabilité dynamique nétait bien sûr toujours pas possible à ce stade, car les stubs manquaient souvent des fonctionnalités nécessaires sous Windows et, par conséquent, le code ne pouvait pas sexécuter à ce stade.)

Obtenir la base de code pour construire avec succès a été une étape importante. Une fois les modifications spécifiques à la plate-forme intégrées, toute léquipe assumerait désormais la responsabilité dassurer la portabilité statique de toutes les modifications futures et lintroduction de modifications incompatibles serait minimisée dans lensemble de la base de code. (La portabilité dynamique nétait bien sûr toujours pas possible à ce stade, car les stubs manquaient souvent des fonctionnalités nécessaires sous Windows et, par conséquent, le code ne pouvait pas sexécuter à ce stade.)

Non seulement cela a réduit la charge de travail, mais cela a permis à leffort de portabilité de Windows de ralentir et de procéder plus lentement et avec précaution pour résoudre en profondeur des incompatibilités plus difficiles, sans avoir besoin de détourner lattention des changements de rupture constamment introduits dans la base de code. Cela a permis des solutions de meilleure qualité sur le long terme, y compris une refactorisation de la base de code qui a potentiellement affecté la sémantique sur dautres plates-formes.

Compatibilité statique (principalement C / C ++)

Cétait peut-être létape la plus longue, à laquelle les stubs de compatibilité seraient supprimés ou élaborés, le code désactivé pourrait être réactivé et les problèmes individuels pourraient être résolus. La nature statique du C ++ permettait aux diagnostics du compilateur de guider la plupart de ces changements nécessaires sans avoir à exécuter de code du tout.

Les changements individuels ici, y compris certains mentionnés précédemment, seraient trop longs pour être discutés en profondeur dans leur intégralité, et certains problèmes individuels mériteraient probablement leurs propres longs articles de blog. La création et la gestion des processus, par exemple, est en fait une entreprise étonnamment complexe pleine d’idiosyncrasies et d’embûches (voir ici par exemple) pour laquelle il ne semble pas y avoir de bon, bibliothèque multiplateforme, bien que ce soit un vieux problème. Cela est dû en partie à de mauvaises conceptions initiales dans les API du système dexploitation elles-mêmes. ( Le code source de Chromium donne un aperçu de certaines des complexités qui sont souvent ignorées ailleurs. En effet, le code source de Chromium peut souvent servir dillustration de la manipulation beaucoup dincompatibilités et de subtilités de plate-forme.) Notre ignorance initiale de ce fait nous a conduit à essayer dutiliser Boost.Process, mais le manque de sémantique de propriété claire pour POSIX pid_t (qui est utilisé pour les deux processus didentification et de propriété) ainsi que des bogues dans la bibliothèque Boost.Process elle-même a rendu difficile la recherche de bogues dans la base de code, ce qui a finalement conduit à notre décision dannuler ce changement et dintroduire notre propre abstraction. De plus, la bibliothèque Boost.Process était assez lourde même pour une bibliothèque Boost, ce qui a considérablement ralenti nos builds. Au lieu de cela, nous avons fini par écrire notre propre wrapper pour les objets de processus. Cela sest avéré très bien fonctionner pour nos besoins. Lun de nos points à retenir dans ce cas était de envisager dadapter les solutions à nos propres besoins et de ne pas supposer que les solutions préexistantes sont le meilleur choix .

Ce nétait, bien sûr, quun aperçu de la question de la gestion des processus.Certains autres aspects de leffort de portabilité méritent peut-être aussi dêtre développés.

Par Thomas Jensen

Certaines parties de la base de code (comme la communication avec le serveur Plasma dans Arrow) sont supposées la possibilité dutiliser des sockets de domaine UNIX (AF_UNIX) sous Windows. Alors que les versions récentes de Windows 10 prennent en charge les sockets du domaine UNIX, les implémentations ne sont ni suffisantes pour couvrir toutes les utilisations possibles du domaine UNIX les sockets, et les sockets du domaine UNIX ne sont pas particulièrement élégants: ils nécessitent un nettoyage manuel et peuvent laisser des fichiers inutiles sur le système de fichiers en cas de terminaison non propre dun processus, et ils ne permettent pas denvoyer des données auxiliaires (telles que descripteurs de fichier) vers un autre processus. Comme Ray utilise Boost.Asio, le remplacement le plus rapide des sockets de domaine UNIX était les sockets TCP locaux (qui pouvaient tous deux être abstraits en tant que sockets généraux), nous sommes donc passés à ce dernier, simplement remplacer les chemins de fichiers par des versions stringized des adresses TCP / IP sans avoir besoin de refactoriser la base de code.

Ce nétait pas suffisant, car lutilisation de sockets TCP ne permettait toujours pas de se répliquer descripteurs de socket dans dautres processus. En effet, une solution appropriée à ce problème aurait été de l’éviter complètement. Étant donné que cela nécessiterait une refactorisation à plus long terme des parties pertinentes de la base de code (une tâche dans laquelle dautres se sont lancés), cependant, une approche transparente semblait plus appropriée dans lintervalle. Cela a été rendu difficile par le fait que, sur les systèmes UNIX, la duplication dun descripteur de fichier ne nécessite pas la connaissance de lidentité du processus cible, alors que, sous Windows, dupliquer un descripteur nécessite de manipuler activement le processus cible. Pour résoudre ces problèmes, nous avons implémenté la possibilité déchanger des descripteurs de fichiers en remplaçant la procédure de prise de contact lors du lancement du magasin Plasma par un mécanisme plus spécialisé qui établirait une connexion TCP , recherchez lID du processus à lautre extrémité (une procédure peut-être lente, mais ponctuelle), dupliquez le handle de socket et informez le processus cible du nouveau handle. Bien que ce ne soit pas une solution à usage général (et en fait peut être sujette à des conditions de course dans le cas général) et une approche assez inhabituelle, cela a bien fonctionné pour les besoins de Ray, et peut être une approche dautres projets confrontés au même problème pourraient en bénéficier.

Au-delà de cela, lun des plus gros problèmes auxquels nous nous attendions à faire face était notre dépendance au serveur Redis . Alors que Microsoft Open Technologies (MSOpenTech) avait précédemment implémenté un port Redis sur Windows, le projet avait été abandonné et ne prenait donc pas en charge les versions de Redis requises par Ray . Cela nous a initialement fait supposer que nous aurions besoin de toujours exécuter le serveur Redis sur le sous-système Windows pour Linux (WSL), ce qui aurait probablement été gênant pour les utilisateurs. Nous avons donc été très reconnaissants de découvrir quun autre développeur avait poursuivi le projet de produire des binaires Redis ultérieurs sur Windows (voir tporadowski / redis ). Cela a considérablement simplifié notre problème et nous a permis de fournir un support natif de Ray pour Windows.

Enfin, les obstacles les plus importants auxquels nous avons été confrontés (comme MSOpenTech Redis, et comme la plupart des autres programmes uniquement POSIX) étaient labsence de substituts triviaux pour certaines API POSIX sous Windows. Certains dentre eux ( tels que

getppid()) étaient simples, bien quun peu fastidieux. Cependant, le problème le plus difficile rencontré pendant tout le processus de portage était probablement celui des descripteurs de fichiers par rapport aux descripteurs de fichiers. Une grande partie du code sur lequel nous nous sommes appuyés (comme le magasin Plasma dans Arrow) supposait lutilisation de descripteurs de fichiers POSIX (int s). Cependant, Windows utilise nativement des HANDLE, qui sont de la taille dun pointeur et analogues à size_t. En soi, cependant, ce nest pas un problème important, car le runtime Microsoft Visual C ++ (CRT) fournit une couche de type POSIX. Cependant, la couche est limitée en fonctionnalités, nécessite des traductions sur chaque site dappel qui ne la prend pas en charge et, en particulier, ne peut pas être utilisée pour des éléments tels que des sockets ou des descripteurs de mémoire partagée. De plus, nous ne voulions pas supposer que les HANDLE seraient toujours suffisamment petits pour tenir dans un entier 32 bits, même si cétait souvent le cas, car il nétait pas clair si des circonstances dont nous nétions pas conscients pourraient briser cette hypothèse en silence.Cela a considérablement aggravé nos problèmes, car la solution la plus évidente aurait été de détecter tous les int qui représentent des descripteurs de fichiers dans une bibliothèque telle que Arrow, et de les remplacer (et toutes leurs utilisations ) avec un autre type de données, qui était un processus sujet aux erreurs et impliquait des correctifs importants au code externe, créant une charge de maintenance importante.

Il était assez difficile de décider quoi faire à ce stade. La solution de MSOpenTech Redis pour le même problème a clairement montré quil sagissait dune tâche assez ardue, car ils avaient résolu ce problème en créer une table de descripteurs de fichiers singleton à léchelle du processus en haut de limplémentation CRT existante, les obligeant à gérer la sécurité des threads, ainsi que les forçant à intercepter toutes utilisations des API POSIX (même celles quelles étaient déjà capables de gérer) simplement pour traduire des descripteurs de fichiers. Au lieu de cela, nous avons décidé dadopter une approche inhabituelle: nous avons étendu la couche de traduction POSIX dans le CRT . Cela a été fait en identifiant les poignées incompatibles au moment de la création et en «poussant» ces poignées dans les tampons dun tube superflu, en retournant le descripteur de ce tube à la place. Nous navons alors eu quà modifier les sites dutilisation de ces descripteurs, qui étaient, surtout, triviaux à identifier, car ils étaient tous socket et API de fichiers mappés en mémoire . En fait, cela a permis déviter le besoin de correctifs, car nous avons simplement pu rediriger de nombreuses fonctions via des macros .

Alors que leffort de développer cette couche dextension unique (dans win32fd.h) était significative (et assez peu orthodoxe), cela en valait probablement la peine, comme la couche de traduction était en fait assez petit en comparaison, et nous permettait de déléguer la plupart des problèmes indépendants (tels que le verrouillage multithread de la table des descripteurs de fichiers) aux API CRT. De plus, en exploitant les canaux anonymes du même tableau global de descripteurs de fichiers (malgré notre manque daccès direct à celui-ci), nous avons pu éviter davoir à intercepter et à traduire descripteurs de fichiers pour dautres fonctions qui pourraient déjà être gérées directement. Cela a permis à une grande partie du code de rester essentiellement inchangée avec un impact minimal sur les performances, jusquà ce que nous ayons plus tard la possibilité de refactoriser le code et de fournir de meilleurs wrappers à un niveau supérieur (comme via les wrappers Boost.Asio). Il est tout à fait possible quune extension de cette couche puisse permettre à dautres projets tels que Redis dêtre portés sur Windows de manière beaucoup plus transparente et avec des changements beaucoup moins drastiques ou un potentiel de bogues.

Par Andrea Leopardi

Exécutabilité à lexécution (preuve de concept)

Une fois que nous avons cru que le noyau de Ray fonctionnait correctement, létape suivante était de sassurer quun test Python pouvait réussir à exercer nos chemins de code. Au départ, nous navions pas donné la priorité à le faire. Cependant, cela sest avéré être une erreur, car des modifications ultérieures par dautres développeurs de léquipe ont en fait introduit des incompatibilités plus dynamiques avec Windows, et le système CI na pas été en mesure de détecter de telles ruptures. Nous avons donc par la suite fait une priorité de exécuter un test minimal sur les builds Windows, pour éviter dautres ruptures de la build.

Pour le la plupart du temps, nos efforts ont été couronnés de succès, et tous les bogues persistants dans le noyau de Ray se trouvaient dans des parties prévisibles de la base de code (bien que les résoudre exigeait souvent de passer par du code multi-processus, ce qui était loin dêtre trivial). Cependant, il y a eu au moins une surprise quelque peu désagréable en cours de route du côté C et une du côté Python, qui nous ont tous deux encouragés (entre autres) à lire la documentation du logiciel de manière plus proactive à lavenir.

Du côté C, notre wrapper pour léchange de poignées de socket reposait sur le remplacement naïf de sendmsg et recvmsg avec WSASendMsg et WSARecvMsg. Ces API Windows étaient les équivalents les plus proches des API POSIX et semblaient donc être un choix évident. Cependant, lors de lexécution, le code plantait constamment et la source du problème nétait pas claire. Certains débogages (y compris avec les versions de débogage des versions & runtimes) ont permis de révéler que le problème provenait des variables de pile transmises à WSASendMsg. Un débogage plus poussé et une inspection approfondie du contenu de la mémoire suggéraient que le problème était peut-être le champ msg_flags de WSAMSG, car cétait le seul non initialisé champ.Cependant, cela ne semblait pas pertinent: msg_flags a simplement été traduit de flags dans struct msghdr, qui na pas été utilisé en entrée, et a été simplement utilisé comme paramètre de sortie. La lecture de la documentation a cependant révélé le problème: sous Windows, le champ servait également de paramètre input , et le laisser ainsi non initialisé entraînait un comportement imprévisible! Cela était assez inattendu pour nous et a abouti à deux points importants à retenir: lire attentivement la documentation de chaque fonction , et de plus, que initialiser les variables ne consiste pas seulement à garantir lexactitude avec les API actuelles, mais aussi à rendre le code robuste aux modifications futures des API ciblées .

Du côté de Python, nous avons rencontré un problème différent. Nos modules Python natifs échoueraient initialement à se charger, malgré labsence de problèmes évidents. Après plusieurs jours de devinettes, de parcourir lassemblage et le code source CPython, et dinspecter les variables dans la base de code CPython, il est devenu évident que le problème était labsence dun suffixe .pyd sur Python dynamique bibliothèques sous Windows. En fin de compte, pour des raisons qui ne sont pas claires pour nous, Python refuse de charger même les fichiers .dll sous Windows en tant que modules Python, malgré le fait que les bibliothèques partagées natives pourraient normalement être chargées même avec nimporte quel fichier extension. En effet, il sest avéré que ce fait était documenté sur le site Web de Python . Malheureusement, cependant, la présence dune telle documentation ne pouvait pas impliquer notre réalisation de la rechercher.

Néanmoins, finalement, Ray a pu fonctionner avec succès sous Windows, ce qui a conclu le prochain jalon et a fourni une preuve du concept pour leffort.

Par Hitesh Choudhary

Compatibilité dexécution (principalement Python)

À ce stade, une fois que le cœur de Ray était en travaillant, nous avons pu nous concentrer sur le portage de code de plus haut niveau. Certains problèmes étaient assez faciles à résoudre – par exemple, certaines API Python qui sont uniquement UNIX (par exemple, os.uname()[1]) ont souvent des remplacements appropriés sous Windows (comme socket.gethostname()), et les trouver était une question de savoir en rechercher toutes les instances dans la base de code. Dautres problèmes étaient plus difficiles à localiser ou à résoudre. Parfois, ils étaient dus à lutilisation de commandes spécifiques à POSIX (telles que ps), qui nécessitaient des approches alternatives (telles que lutilisation de psutil pour Python). Dautres fois, ils étaient dus à des incompatibilités dans des bibliothèques tierces. Par exemple, lorsquun socket se déconnecte sous Windows, une erreur est déclenchée, plutôt que dentraîner des lectures vides. La bibliothèque Python pour Redis ne semblait pas gérer cela. De telles différences de comportement nécessitaient un patching monkey explicite pour éviter des erreurs parfois déroutantes qui se produiraient à la fin de Ray.

Bien que certains de ces problèmes soient assez fastidieux mais probablement attendu (comme le remplacement des utilisations de /tmp par le répertoire temporaire de la plate-forme, ou en évitant de supposer que tous les chemins absolus commencent par une barre oblique), certains étaient quelque peu inattendus (comme conflits de réservations de port ) ou (comme cest souvent le cas) en raison dhypothèses erronées, et leur compréhension dépend de la compréhension de larchitecture de Windows et de ses approches de compatibilité descendante.

Une telle histoire tourne autour de lutilisation de barres obliques comme séparateurs de répertoires sous Windows. En général, ceux-ci semblent fonctionner correctement et sont couramment utilisés par les développeurs. Cependant, cela est en fait dû à la conversion automatique des barres obliques en barres obliques inverses dans les bibliothèques du sous-système Windows en mode utilisateur, et certains traitements automatiques peuvent être supprimés en préfixant explicitement les chemins avec un préfixe \\?\, ce qui est utile pour contourner certaines fonctionnalités de compatibilité (telles que les longs chemins). Cependant, nous nutilisions jamais explicitement un tel chemin, et nous avons supposé que les utilisateurs devraient éviter une utilisation inhabituelle dans nos versions expérimentales. Cependant, il est apparu plus tard que lorsque Bazel invoquait certains tests Python, les chemins seraient traités dans ce format pour permettre lutilisation de longs chemins, ce qui désactivait la traduction automatique sur laquelle nous nous appuyions implicitement. Cela nous a conduit à des points à retenir importants: premièrement, il est généralement préférable dutiliser les API de la manière la plus appropriée pour le système cible, car cela offre le moins de possibilités de problèmes inattendus. .Deuxièmement, et surtout, il est tout simplement faux de supposer que l’environnement d’un utilisateur est prévisible . La réalité est que les logiciels modernes reposent presque toujours sur lexécution de code tiers dont nous ignorons les comportements précis. Même si lon peut supposer quun utilisateur évite des situations problématiques, les logiciels tiers ignorent complètement ces hypothèses cachées. Il est donc probable quils se produiront de toute façon, non seulement pour les utilisateurs, mais aussi pour les développeurs de logiciels eux-mêmes, ce qui entraînera des bogues plus difficiles à localiser que à résoudre lors de lécriture du code initial. Par conséquent, il est important d’éviter d’accorder trop de poids à la convivialité du programme (le contraire de la «convivialité») lors de la conception d’un système robuste.

(Par ailleurs, sous Windows, les chemins peuvent en fait contenir des guillemets et de nombreux autres caractères spéciaux qui sont normalement considérés comme illégaux. Cela se produit lors de lutilisation de flux de données alternatifs NTFS. Cependant, ils sont suffisamment rares et complexes pour que même les bibliothèques de langage standard ne les gèrent souvent pas.)

Une fois les problèmes les plus importants résolus, cependant, de nombreux tests ont pu passer sous Windows, créant la première implémentation Windows expérimentale de Ray.

Par Ross Sneddon

Améliorations à lexécution (par exemple, prise en charge dUnicode)

À ce stade, une grande partie du cœur de Ray peut être utilisée sur Windows comme sur dautres plates-formes. Néanmoins, certains problèmes demeurent, qui nécessitent des efforts continus pour les résoudre.

Le support Unicode est lun de ces problèmes. Pour des raisons historiques, le sous-système en mode utilisateur Windows a deux versions de la plupart des API: une version «ANSI» qui prend en charge les jeux de caractères à un octet et une version «Unicode» qui prend en charge UCS-2 ou UTF-16 (selon le détails de lAPI en question). Malheureusement, aucun de ceux-ci nest UTF-8; même la prise en charge de base dUnicode nécessite lutilisation de chaînes de caractères larges (basées sur wchar_t) sur toute la base de code. (nb: en fait, Microsoft a récemment tenté dintroduire UTF-8 en tant que page de codes, mais il nest pas suffisamment pris en charge pour résoudre ce problème de manière transparente, du moins sans sappuyer sur des composants internes Windows potentiellement non documentés et fragiles.)

Traditionnellement, les programmes Windows gèrent Unicode grâce à lutilisation de macros telles que _T() ou TEXT(), qui sétendent à étroit ou large -character literals selon quune version Unicode est spécifiée et utiliser TCHAR comme types de caractères génériques. De même, la plupart des API C ont des versions TCHAR dépendantes (telles que _tcslen() au lieu de strlen()) pour permettre la compatibilité avec les deux types de code. Cependant, la migration dune base de code UNIX vers ce modèle est une entreprise plutôt complexe. Cela na pas encore été fait dans Ray, et par conséquent, au moment décrire ces lignes, Ray ne prend pas en charge Unicode approprié dans (par exemple) les chemins de fichiers sous Windows, et la meilleure approche pour le faire peut encore être une question ouverte.

Un autre problème de ce type est le mécanisme de communication inter-processus. Bien que les sockets TCP puissent fonctionner correctement sous Windows, ils sont sous-optimaux, car ils introduisent une couche de complexité inutile dans la logique (comme les délais dexpiration, les keep-alives, lalgorithme de Nagle), peuvent entraîner une accessibilité accidentelle à partir dhôtes non locaux, et peuvent introduire une surcharge de performance. À lavenir, les canaux nommés pourraient fournir un meilleur remplacement pour les sockets de domaine UNIX sous Windows; en fait, même sous Linux, les tubes ou les soi-disant abstract sockets de domaine UNIX peuvent également savérer être de meilleures alternatives, car ils ne nécessitent pas dencombrement et de nettoyage des fichiers de socket sur le système de fichiers.

Enfin, un autre exemple dun tel problème est la compatibilité des sockets BSD, ou plutôt son absence. Une excellente réponse sur StackOverflow aborde certains des problèmes en profondeur, mais brièvement, alors que les API de socket communes sont des dérivés de lAPI de socket BSD dorigine, différentes plates-formes implémentent un socket similaire drapeaux différemment. En particulier, les conflits avec les adresses IP ou les ports TCP existants peuvent produire des comportements différents selon les plates-formes. Bien que les problèmes soient difficiles à détailler ici, le résultat final est que cela peut rendre difficile lutilisation simultanée de plusieurs instances de Ray sur le même hôte. (En fait, comme cela dépend du comportement du noyau du système dexploitation, cela affecte également WSL.) Cest encore un autre problème connu dont la solution est plutôt complexe et pas complètement traitée dans le système actuel.

Conclusion

Le processus de portage dune base de code comme celle de Ray vers Windows a été une expérience précieuse qui met en évidence les avantages et les inconvénients de nombreux aspects du développement logiciel et leur impact sur la maintenance du code.La description précédente ne met en évidence que quelques-uns des obstacles rencontrés en cours de route. De nombreuses conclusions utiles peuvent être tirées du processus, dont certaines peuvent être utiles à partager ici pour dautres projets espérant atteindre un objectif similaire.

Premièrement, dans certains cas, nous avons en fait découvert plus tard que les versions suivantes de certaines bibliothèques (comme hiredis) avaient déjà résolu certains problèmes que nous avions résolus. Les solutions nétaient pas toujours évidentes, car (par exemple) la version de hiredis dans les versions récentes de Redis était en fait une copie périmée de hiredis, ce qui nous amène à penser que certains problèmes navaient pas encore été résolus. Les révisions ultérieures nont pas non plus toujours résolu pleinement tous les problèmes de compatibilité existants. Néanmoins, il aurait peut-être économisé un peu defforts pour vérifier plus en profondeur les solutions existantes à certains problèmes pour éviter davoir à les résoudre à nouveau.

Par John Barkiple

Deuxièmement, la chaîne dapprovisionnement des logiciels est souvent complexe . Les bogues peuvent naturellement saggraver à chaque couche, et il est faux de croire que même les outils open source largement utilisés seront «testés au combat» et donc robustes, en particulier lorsquils sont utilisés dans colère . De plus, de nombreux problèmes de génie logiciel de longue date ou courants ne disposent pas de solutions satisfaisantes pour lutilisation, en particulier (mais pas seulement) lorsquils nécessitent une compatibilité entre différents systèmes. En effet, en dehors de simples bizarreries, dans le processus de portage de Ray sur Windows, nous avons régulièrement rencontré et souvent signalé des bogues dans de nombreux logiciels, y compris mais sans sy limiter un bug Git Linux qui a affecté lutilisation de Bazel, Redis (Linux) , glog , psutil (bogue danalyse qui affecte WSL) , grpc , beaucoup bogues difficiles à identifier dans Bazel lui-même (par exemple, 1 , 2 , 3 , 4 ), Travis CI et Actions GitHub , entre autres. Cela nous a également encouragés à accorder une plus grande attention à la complexité de nos dépendances.

Troisièmement, investir dans loutillage et linfrastructure rapporte des dividendes à long terme. Des constructions plus rapides permettent un développement plus rapide, et des outils plus puissants permettent de résoudre plus facilement des problèmes complexes. Dans notre cas, lutilisation de Bazel nous a aidés à bien des égards, bien quil soit loin dêtre parfait et quil impose une courbe dapprentissage abrupte. Investir du temps (éventuellement plusieurs jours) pour apprendre les capacités, les forces et les faiblesses dun nouvel outil est rarement facile, mais susceptible dêtre bénéfique pour la maintenance du code. Dans notre cas, passer un peu de temps à lire en profondeur la documentation Bazel nous a permis didentifier beaucoup plus rapidement une multitude de problèmes et de solutions à venir. De plus, cela nous a également aidés à intégrer des outils avec Bazel que peu dautres avaient apparemment réussi à faire, comme loutil include-what-you-use de Clang.

Quatrièmement, et comme mentionné précédemment, il est prudent dadopter des pratiques de codage sécurisées telles que linitialisation de la mémoire avant utilisation lorsque il ny a pas de compromis significatif. Même lingénieur le plus prudent ne peut pas nécessairement prédire lévolution future du système sous-jacent qui pourrait invalider silencieusement les hypothèses.

Enfin, comme cest généralement le cas en médecine, prévention est le meilleur remède . La prise en compte des développements futurs possibles et le codage vers des interfaces standardisées permettent une conception de code plus extensible que ce qui peut être facilement réalisé après lapparition dincompatibilités.

Bien que le portage de Ray vers Windows ne soit pas encore terminé, il a été tout à fait réussie jusquà présent, et nous espérons que le partage de notre expérience et de nos solutions pourra servir de guide utile aux autres développeurs qui envisagent de se lancer dans une aventure similaire.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *