Ray porten naar Microsoft Windows

Door Johny vino

(Mehrdad Niknami) (28 sep.2020)

Background

Toen Ray voor het eerst werd gelanceerd, was het geschreven voor Linux en macOS – UNIX-gebaseerde besturingssystemen. Ondersteuning voor Windows, het meest populaire desktop-besturingssysteem, ontbrak, maar was belangrijk voor het succes van het project op de lange termijn. Hoewel het Windows-subsysteem voor Linux (WSL) een mogelijke optie bood voor sommige gebruikers, beperkte het de ondersteuning tot recente versies van Windows 10 en werd het veel moeilijker voor code om te communiceren met het native Windows-besturingssysteem, wat neerkwam op een slechte ervaring voor gebruikers. Met het oog hierop hoopten we echt native Windows-ondersteuning te bieden aan gebruikers, als dit al praktisch was.

Ray porten naar Windows was echter verre van een triviale taak. Net als veel andere projecten die achteraf draagbaarheid trachten te bereiken, kwamen we veel uitdagingen tegen waarvan de oplossingen niet voor de hand lagen. In deze blogpost willen we een diepe duik nemen in de technische details van het proces dat we hebben gevolgd om Ray compatibel te maken met Windows. We hopen dat het andere projecten met een vergelijkbare visie kan helpen om enkele van de mogelijke uitdagingen te begrijpen en hoe deze kunnen worden aangepakt.

Overzicht

Het achteraf aanbrengen van ondersteuning voor Windows zorgde voor een steeds grotere uitdaging naarmate de ontwikkeling van Ray verder vorderde. Voordat we begonnen, verwachtten we dat bepaalde aspecten van de code een aanzienlijk deel van onze inspanningen op het gebied van overdraagbaarheid zouden uitmaken, waaronder de volgende:

  • Communicatie tussen processen (IPC) en gedeeld geheugen
  • Objecthandvatten en bestandshandvatten / descriptors (FDs)
  • Procesuitbreiding en beheer, inclusief signalering
  • Bestandsbeheer, threadbeheer en asynchrone I / O
  • Gebruik van shell- en systeemopdrachten
  • Redis

Gezien de omvang van de problemen, realiseerden we ons al snel dat het voorkomen van verdere incompatibiliteiten voorrang moest krijgen boven het proberen om bestaande problemen. We hebben dus geprobeerd om door te gaan in ongeveer de volgende stappen, hoewel dit enigszins een vereenvoudiging is, aangezien specifieke problemen soms in verschillende fasen werden aangepakt:

  1. Compatibiliteit voor afhankelijkheden van derden
  2. Compileerbaarheid voor Ray (via lege stubs & TODOs)
  3. Koppelbaarheid
  4. Continue integratie (CI) (om verdere incompatibele wijzigingen te blokkeren)
  5. Statische compatibiliteit (meestal C ++)
  6. Run-time uitvoerbaarheid (minimale POC)
  7. Run-time compatibiliteit (meestal Python)
  8. Run -time verbeteringen (bijv. Unicode-ondersteuning)
Door Ashkan Forouzani

Ontwikkelingsproces

Op een hoog niveau kunnen de benaderingen hier variëren. Voor kleinere projecten kan het zeer voordelig zijn om de codebase te vereenvoudigen terwijl de code wordt overgezet naar een nieuw platform. De aanpak die we hebben gevolgd, die erg nuttig bleek voor een grote en dynamisch veranderende codebase, was om problemen een voor een aan te pakken , houd wijzigingen zo orthogonaal mogelijk ten opzichte van elkaar , en geef prioriteit aan behoud van semantiek boven eenvoud .

Soms vereiste dit het schrijven van mogelijk externe code om omstandigheden te behandelen die niet noodzakelijkerwijs in de productie waren opgetreden (bijvoorbeeld door een bestandspad dat nooit spaties zou hebben). Op andere momenten vereiste dit de introductie van meer algemene, mechanische oplossingen die mogelijk vermeden konden worden (zoals het gebruik van een std::shared_ptr voor bepaalde gevallen waarin een ander ontwerp mag een std::unique_ptr worden gebruikt). Het behouden van de semantiek van de bestaande code was echter absoluut cruciaal om de constante introductie van nieuwe bugs in de codebase te vermijden, wat de rest van het team zou hebben getroffen. En achteraf gezien was deze aanpak behoorlijk succesvol: bugs op andere platforms als gevolg van Windows-gerelateerde wijzigingen waren vrij zeldzaam, en kwamen het vaakst voor als gevolg van veranderingen in afhankelijkheden dan als gevolg van semantische veranderingen in de codebase.

Compileerbaarheid (afhankelijkheden van derden)

De eerste hindernis was ervoor te zorgen dat afhankelijkheden van derden zelf op Windows konden worden gebouwd. Hoewel veel van onze afhankelijkheden veelgebruikte bibliotheken waren en de meeste geen grote incompatibiliteiten hadden, was dit niet altijd eenvoudig. In het bijzonder waren er toevallige complexiteiten in overvloed in verschillende facetten van het probleem.

  • De build-bestanden (in het bijzonder Bazel-build-bestanden) voor sommige projecten waren soms ontoereikend op Windows, waardoor patches nodig waren. Deze waren vaak te wijten aan problemen die minder voorkwamen op UNIX-platforms, zoals het citeren van bestandspaden met spaties, van het starten van de juiste Python-interpreter, of de kwestie van het correct koppelen van gedeelde bibliotheken versus statische bibliotheken. Gelukkig was Bazel zelf een van de meest nuttige tools om dit probleem op te lossen: de ingebouwde mogelijkheid om externe werkruimten te patchen is, zoals we snel leerden, buitengewoon . Het maakte een standaardmethode mogelijk om externe bibliotheken te patchen zonder de buildprocessen op een ad-hoc manier te wijzigen, waardoor de codebase schoon bleef.
  • De build-toolchains vertoonden hun eigen complexiteit, aangezien de compiler- of linkerkeuze vaak van invloed was de bibliotheken, APIs en definities waarop kan worden vertrouwd. En helaas kan het wisselen van compilers op Bazel nogal omslachtig zijn. Met name op Windows komt de beste ervaring vaak met het gebruik van de Microsoft Visual C ++ build-tools, maar onze toolchains op andere platforms waren gebaseerd op GCC of Clang. Gelukkig wordt de LLVM-toolchain geleverd met Clang-Cl op Windows, waardoor een combinatie van Clang- en MSVC-functies kan worden gebruikt, waardoor veel problemen veel gemakkelijker op te lossen zijn.
  • Bibliotheekafhankelijkheden leverden over het algemeen de meeste problemen op. Soms was het probleem zo alledaags als een ontbrekende header of een tegenstrijdige definitie die mechanisch kon worden afgehandeld, waar zelfs veelgebruikte bibliotheken (zoals Boost) niet immuun voor waren. De oplossingen hiervoor waren vaak een paar geschikte macrodefinities of dummy headers. In andere gevallen – zoals voor de bibliotheken hiredis en Arrow bibliotheken – vereisten bibliotheken POSIX-functionaliteit die niet beschikbaar was op Windows (zoals signalen, of de mogelijkheid om bestandsdescriptors door te geven via UNIX-domeinsockets). Deze bleken in sommige gevallen veel moeilijker op te lossen. Omdat we ons in dit stadium meer zorgen maakten over de compileerbaarheid, was het vruchtbaar om de implementatie van complexere APIs uit te stellen naar een later stadium en om basisstubs te gebruiken of de aanstootgevende code uit te schakelen om door te gaan met bouwen.

Zodra we klaar waren met het compileren van afhankelijkheden, konden we ons concentreren op het compileren van de kern van Ray zelf.

Compileerbaarheid (Ray)

De volgende hindernis was om Ray zelf naar compileer, samen met de Plasma -winkel (onderdeel van Arrow ). Dit was moeilijker omdat de code vaak helemaal niet voor het Windows-model was ontworpen en sterk afhankelijk was van POSIX APIs. In sommige gevallen was dit slechts een kwestie van het vinden en gebruiken van de juiste headers (zoals WinSock2.h in plaats van sys/time.h voor struct timeval, wat verrassend kan zijn) of vervangende items maken ( zoals voor

unistd.h ). In andere gevallen hebben we de incompatibele code uitgeschakeld en TODOs overgelaten om in de toekomst te adresseren.

Hoewel dit conceptueel vergelijkbaar was met het geval van omgaan met afhankelijkheden van derden, een unieke zorg op hoog niveau die hier naar voren kwam, was het minimaliseren van wijzigingen in de codebase , in plaats van de meest elegante oplossing mogelijk. In het bijzonder, aangezien het team de codebase voortdurend bijwerkte in de veronderstelling van een POSIX API en aangezien de codebase nog niet compileerde met Windows, oplossingen die drop-in waren en die transparant konden worden gebruikt met minimale wijzigingen om een ​​uitgebreide compatibiliteit te verkrijgen de hele codebase was veel nuttiger bij het vermijden van syntactische of semantische samenvoegconflicten dan oplossingen die chirurgisch van aard waren. Vanwege dit feit hebben we in plaats van elke aanroepsite te wijzigen, onze eigen equivalente Windows shims voor POSIX APIs gemaakt die het gewenste gedrag simuleerden, zelfs als dat niet optimaal was algemeen. Dit stelde ons in staat om compileerbaarheid te garanderen en de verspreiding van incompatibele wijzigingen veel sneller een halt toe te roepen (en om later elk afzonderlijk probleem uniform in de hele codebase in batch aan te pakken) dan anders mogelijk zou zijn.

Koppelbaarheid

Toen de compileerbaarheid eenmaal was bereikt, was het volgende probleem de juiste koppeling van objectbestanden om uitvoerbare binaire bestanden te verkrijgen.Hoewel ze theoretisch eenvoudig waren, gingen problemen met de koppelingsmogelijkheden vaak gepaard met veel toevallige complexiteiten, zoals de volgende:

  • Bepaalde systeembibliotheken waren platformspecifiek en niet beschikbaar of verschilden op andere platforms ( zoals libpthread)
  • Bepaalde bibliotheken zijn dynamisch gelinkt terwijl verwacht werd dat ze gelinkt waren statisch (of vice versa)
  • Bepaalde symbooldefinities ontbraken of waren tegenstrijdig (bijvoorbeeld connect van hiredis conflicteert met connect voor sockets)
  • Bepaalde afhankelijkheden werkten toevallig op POSIX-systemen, maar vereisten expliciete afhandeling in Bazel op Windows

De oplossingen hiervoor waren vaak alledaagse (hoewel misschien niet voor de hand liggende) wijzigingen in de buildbestanden, hoewel het in sommige gevallen nodig was om afhankelijkheden te patchen. Net als voorheen was Bazels mogelijkheid om praktisch elk bestand van een externe bron te patchen buitengewoon nuttig om dergelijke problemen op te lossen.

Door Richy Geweldig

Het succesvol opbouwen van de codebase was een belangrijke mijlpaal. Zodra de platformspecifieke wijzigingen waren geïntegreerd, zou het hele team nu de verantwoordelijkheid op zich nemen om te zorgen voor statische portabiliteit bij alle toekomstige wijzigingen en de introductie van incompatibele wijzigingen zou in de hele codebase tot een minimum worden beperkt. ( Dynamische portabiliteit was op dit moment natuurlijk nog steeds niet mogelijk, aangezien de stubs vaak niet over de nodige functionaliteit op Windows beschikten en bijgevolg de code in dit stadium niet kon draaien.)

De codebase succesvol laten bouwen was een belangrijke mijlpaal. Zodra de platformspecifieke wijzigingen waren geïntegreerd, zou het hele team nu de verantwoordelijkheid op zich nemen om te zorgen voor statische portabiliteit bij alle toekomstige wijzigingen en de introductie van incompatibele wijzigingen zou in de hele codebase tot een minimum worden beperkt. ( Dynamische portabiliteit was op dit moment natuurlijk nog steeds niet mogelijk, aangezien de stubs vaak niet over de nodige functionaliteit op Windows beschikten en bijgevolg de code in dit stadium niet kon draaien.)

Dit verminderde niet alleen de werklast, maar het zorgde er ook voor dat de portabiliteitsinspanningen van Windows vertraagden en langzamer en voorzichtiger doorgingen om moeilijkere incompatibiliteiten diepgaander aan te pakken, zonder de aandacht af te hoeven leiden van het breken van veranderingen die constant in de codebase werden geïntroduceerd. Dit zorgde voor oplossingen van hogere kwaliteit op de lange termijn, inclusief enige herstructurering van de codebase die mogelijk de semantiek op andere platforms beïnvloedde.

Statische compatibiliteit (meestal C / C ++)

Dit was mogelijk de meest tijdrovende fase, waarin compatibiliteitsstubs zouden worden verwijderd of uitgewerkt, uitgeschakelde code opnieuw zou kunnen worden ingeschakeld en individuele problemen zouden kunnen worden aangepakt. De statische aard van C ++ maakte het voor compilerdiagnostiek mogelijk om de meeste van dergelijke noodzakelijke wijzigingen te begeleiden zonder de noodzaak om enige code uit te voeren.

De individuele wijzigingen hier, inclusief enkele eerder genoemde, zouden te lang zijn om diepgaand te bespreken in hun geheel, en bepaalde individuele problemen zouden waarschijnlijk hun eigen lange blogposts waardig zijn. Het voortbrengen en beheren van processen is bijvoorbeeld in feite een verrassend complexe onderneming vol eigenaardigheden en valkuilen (zie bijvoorbeeld hier ) waarvoor geen goed lijkt te zijn, platformonafhankelijke bibliotheek, ondanks dat dit een oud probleem is. Een deel hiervan is te wijten aan slechte initiële ontwerpen in APIs van het besturingssysteem zelf. ( De Chromium-broncode biedt een glimp van enkele van de complexiteiten die elders vaak worden genegeerd. De Chromium-broncode kan inderdaad vaak dienen als een uitstekende illustratie van het gebruik veel platformoncompatibiliteit en subtiliteiten.) Onze aanvankelijke minachting voor dit feit bracht ons ertoe te proberen Boost.Process te gebruiken, maar het gebrek aan duidelijke eigendomssemantiek voor POSIXs pid_t (die voor beide wordt gebruikt procesidentificatie en eigendom) evenals bugs in de Boost. procesbibliotheek zelf resulteerde in moeilijk te vinden bugs in de codebase, wat uiteindelijk resulteerde in onze beslissing om deze wijziging ongedaan te maken en onze eigen abstractie te introduceren. Bovendien was de Boost.Process-bibliotheek behoorlijk zwaar, zelfs voor een Boost-bibliotheek, en dit vertraagde onze build aanzienlijk. In plaats daarvan hebben we onze eigen wrapper geschreven voor procesobjecten. Dit bleek voor onze doeleinden vrij goed te werken. Een van onze afhaalrestaurants in dit geval was om te overwegen om oplossingen af ​​te stemmen op onze eigen behoeften en niet aan te nemen dat reeds bestaande oplossingen de beste keuze zijn .

Dit was natuurlijk slechts een glimp van de kwestie van procesbeheer.Enkele andere aspecten van de portabiliteitsinspanning zijn misschien ook de moeite waard om verder uit te werken.

Door Thomas Jensen

Gedeelten van de codebase (zoals communicatie met de Plasma-server in Arrow) de mogelijkheid om UNIX-domeinsockets (AF_UNIX) op Windows te gebruiken. Hoewel recente versies van Windows 10 ondersteuning bieden voor UNIX-domeinsockets, zijn de implementaties niet voldoende om alle mogelijke toepassingen van UNIX-domein te dekken sockets, noch zijn UNIX-domeinsockets bijzonder elegant: ze vereisen handmatige opschoning en kunnen onnodige bestanden op het bestandssysteem achterlaten in het geval van een niet-schone beëindiging van een proces, en ze bieden niet de mogelijkheid om aanvullende gegevens te verzenden (zoals bestandsdescriptors) naar een ander proces. Aangezien Ray Boost.Asio gebruikt, was de meest geschikte vervanging voor UNIX-domeinsockets lokale TCP-sockets (die beide kunnen worden geabstraheerd als algemene sockets), dus schakelden we over naar de laatste, alleen het vervangen van bestandspaden door stringized versies van TCP / IP-adressen zonder de codebase te hoeven refactoren.

Dit was niet voldoende, aangezien het gebruik van TCP-sockets nog steeds niet de mogelijkheid bood om te repliceren socket descriptors in andere processen. Een goede oplossing voor dit probleem zou zijn geweest om het helemaal te vermijden. Omdat daarvoor een refactoring op langere termijn van de relevante delen van de codebase nodig zou zijn (een taak waaraan anderen zijn begonnen), leek een transparante aanpak in de tussentijd geschikter. Dit werd bemoeilijkt door het feit dat het dupliceren van een bestandsdescriptor op UNIX-systemen niet kennis van de identiteit van het doelproces vereist, terwijl het dupliceren van een handle in Windows het actief manipuleren van het doelproces vereist. doelproces. Om deze problemen op te lossen, hebben we de mogelijkheid geïmplementeerd om bestandsdescriptors uit te wisselen door de handshake-procedure bij het starten van de Plasma-winkel te vervangen door een meer gespecialiseerd -mechanisme dat een TCP-verbinding tot stand zou brengen , zoek de ID van het proces aan de andere kant op (een misschien langzame, maar eenmalige procedure), dupliceer de socket-handle en informeer het doelproces over de nieuwe handle. Hoewel dit geen oplossing voor algemeen gebruik is (en in het algemeen in feite vatbaar is voor race-omstandigheden) en een nogal ongebruikelijke benadering, werkte het goed voor de doeleinden van Ray en kan het een benadering zijn voor andere projecten die met hetzelfde probleem kampen. kunnen hiervan profiteren.

Daarnaast was een van de grootste problemen die we verwachtten tegen te komen, onze Redis serverafhankelijkheid. Hoewel Microsoft Open Technologies (MSOpenTech) eerder een Redis-poort naar Windows had geïmplementeerd, was het project verlaten en ondersteunde het bijgevolg niet de versies van Redis die Ray nodig had . Dit deed ons aanvankelijk aannemen dat we de Redis-server nog steeds op het Windows-subsysteem voor Linux (WSL) moesten draaien, wat waarschijnlijk onhandig zou zijn geweest voor gebruikers. We waren dus heel dankbaar om te ontdekken dat een andere ontwikkelaar het project had voortgezet om latere Redis-binaire bestanden op Windows te produceren (zie tporadowski / redis ). Dit vereenvoudigde ons probleem enorm en stelde ons in staat native ondersteuning van Ray for Windows te bieden.

Ten slotte waren mogelijk de belangrijkste hindernissen waarmee we werden geconfronteerd (net als MSOpenTech Redis, en net als de meeste andere POSIX-only-programmas) de afwezigheid van triviale vervangers voor sommige POSIX APIs op Windows. Sommige hiervan (, zoals

getppid()) waren rechttoe rechtaan, hoewel enigszins vervelend. Mogelijk het moeilijkste probleem dat zich tijdens het hele porteringsproces tegenkwam, was echter dat van bestandsdescriptors versus bestandshandvatten. Veel van de code waarop we vertrouwden (zoals de Plasma-winkel in Arrow) ging uit van het gebruik van POSIX-bestandsdescriptors (int s). Windows gebruikt echter native HANDLE s, die de grootte van een pointer hebben en analoog zijn aan size_t. Op zich is dit echter geen significant probleem, aangezien de Microsoft Visual C ++ runtime (CRT) een POSIX-achtige laag biedt. De laag is echter beperkt in functionaliteit, vereist vertalingen op elke call-site die dit niet ondersteunt, en kan in het bijzonder niet worden gebruikt voor zaken als sockets of gedeelde geheugenhandvatten. Bovendien wilden we er niet van uitgaan dat HANDLE s altijd klein genoeg zou zijn om in een 32-bits geheel getal te passen, ook al was dit vaak het geval, aangezien het onduidelijk was of omstandigheden waarvan we ons niet bewust waren, konden deze veronderstelling in stilte doorbreken.Dit verergerde onze problemen aanzienlijk, aangezien de meest voor de hand liggende oplossing zou zijn geweest om alle int s te detecteren die bestandsdescriptors vertegenwoordigen in een bibliotheek zoals Arrow, en om ze te vervangen (en al hun toepassingen ) met een alternatief gegevenstype, dat een foutgevoelig proces was en aanzienlijke patches voor externe code met zich meebracht, wat een aanzienlijke onderhoudslast veroorzaakte.

Het was in dit stadium vrij moeilijk om te beslissen wat te doen. De oplossing van MSOpenTech Redis voor hetzelfde probleem maakte duidelijk dat dit een nogal ontmoedigende taak was, aangezien ze dit probleem hadden opgelost door het maken van een enkele, procesbrede bestandsdescriptortabel boven van de bestaande CRT-implementatie, waarbij ze moeten omgaan met thread-safety, en ze moeten dwingen alle toepassingen van POSIX APIs (zelfs degene die het al kon verwerken) alleen om bestandsdescriptors te vertalen. In plaats daarvan hebben we besloten om een ​​ongebruikelijke aanpak te volgen: we breidden de POSIX-vertaallaag in de CRT uit. Dit werd gedaan door incompatibele handvatten op het moment van creatie te identificeren en die handvatten in de buffers van een overtollige pijp te “duwen”, waarbij in plaats daarvan de descriptor van die pijp werd geretourneerd. We hoefden toen alleen de gebruikssites van deze handvatten te wijzigen, die, cruciaal, triviaal waren om te identificeren, aangezien ze allemaal socket en geheugen-toegewezen bestand APIs. Dit hielp in feite de noodzaak van patchen te vermijden, aangezien we in staat waren om veel functies alleen om te leiden via macros .

Hoewel de moeite om ontwikkeling van deze unieke extensielaag (in win32fd.h) was significant (en nogal onorthodox), het was waarschijnlijk de moeite waard, aangezien de vertaallaag was in feite vrij klein in vergelijking, en stelde ons in staat om de meeste niet-gerelateerde problemen (zoals multithreaded vergrendeling van de bestandsdescriptortabel) te delegeren aan de CRT-APIs. Door gebruik te maken van anonieme leidingen uit dezelfde globale bestandsdescriptortabel (ondanks dat we er geen directe toegang toe hadden), konden we voorkomen dat we moesten onderscheppen en vertalen bestandsdescriptors voor andere functies die al direct kunnen worden afgehandeld. Hierdoor kon veel van de code in wezen ongewijzigd blijven met een minimale impact op de prestaties, totdat we later de kans hadden om de code te refactoren en betere wrappers op een hoger niveau te bieden (zoals via Boost.Asio wrappers). Het is heel goed mogelijk dat een uitbreiding van deze laag ervoor zorgt dat andere projecten, zoals Redis, veel naadloos naar Windows worden overgezet en met veel minder ingrijpende wijzigingen of mogelijke bugs.

Door Andrea Leopardi

Run-time Uitvoerbaarheid (Proof-of-Concept)

Toen we eenmaal dachten dat de Ray-kern naar behoren functioneerde, was de volgende mijlpaal om ervoor te zorgen dat een Python-test met succes onze codepaden kon oefenen. Aanvankelijk gaven we hier geen prioriteit aan. Dit bleek echter een vergissing te zijn, aangezien latere wijzigingen door andere ontwikkelaars in het team in feite meer dynamische incompatibiliteit met Windows introduceerden en het CI-systeem dergelijke breuken niet kon detecteren. We hebben er vervolgens een prioriteit van gemaakt om een minimale test uit te voeren op de Windows-builds, om verdere breuken van de build te voorkomen.

Voor de voor het grootste deel waren onze inspanningen succesvol, en eventuele aanhoudende bugs in de Ray-kern bevonden zich in voorspelbare delen van de codebase (hoewel het vaak nodig was om door multiprocescode te stappen, wat verre van triviaal was). Er was echter tenminste één ietwat onaangename verrassing onderweg aan de C-kant en één aan de Python-kant, die ons beide (onder andere) aanmoedigden om in de toekomst de softwaredocumentatie proactiever te lezen.

Aan de C-kant was onze eerste handshake wrapper voor het uitwisselen van socketgrepen gebaseerd op het naïef vervangen van sendmsg en recvmsg met WSASendMsg en WSARecvMsg. Deze Windows APIs waren de beste equivalenten van de POSIX APIs en leken dus een voor de hand liggende keuze. Bij uitvoering crashte de code echter constant en was de oorzaak van het probleem onduidelijk. Sommige foutopsporing (inclusief foutopsporingsversies van builds & runtimes) hielpen onthullen dat het probleem lag bij de stackvariabelen die werden doorgegeven aan WSASendMsg. Verdere foutopsporing en nauwkeurige inspectie van de geheugeninhoud suggereerde dat het probleem mogelijk het msg_flags -veld van WSAMSG was, aangezien dit het enige niet-geïnitialiseerde veld.Dit leek echter irrelevant te zijn: msg_flags is alleen vertaald van flags in struct msghdr, die niet werd gebruikt bij invoer, en alleen werd gebruikt als een uitvoerparameter. Het lezen van de documentatie bracht echter het probleem aan het licht: in Windows diende het veld ook als een input -parameter, en het niet-geïnitialiseerd laten, resulteerde in onvoorspelbaar gedrag! Dit was nogal onverwacht voor ons, en resulteerde in twee belangrijke maatregelen om vooruit te komen: om de documentatie van elke functie zorgvuldig te lezen , en bovendien gaat het initialiseren van variabelen niet alleen over het verzekeren van correctheid met de huidige APIs, maar ook belangrijk om code robuust te maken voor toekomstige wijzigingen aan de beoogde APIs .

Aan de Python-kant kwamen we een ander probleem tegen. Onze native Python-modules zouden aanvankelijk niet laden, ondanks het ontbreken van duidelijke problemen. Na meerdere dagen van giswerk, het doorlopen van assemblage en de CPython-broncode en het inspecteren van variabelen in de CPython-codebase, werd het duidelijk dat het probleem het ontbreken van een .pyd -achtervoegsel op dynamische Python was bibliotheken op Windows. Het blijkt dat Python om onduidelijke redenen weigert zelfs .dll bestanden op Windows te laden als Python-modules, ondanks het feit dat native gedeelde bibliotheken normaal gesproken zelfs met elk bestand kunnen worden geladen uitbreiding. Het bleek inderdaad dat dit feit was gedocumenteerd op de website van Python . Helaas kon de aanwezigheid van dergelijke documentatie onmogelijk impliceren dat we ons realiseerden om ernaar te zoeken.

Desalniettemin was Ray uiteindelijk in staat om met succes op Windows te draaien, en dit sloot de volgende mijlpaal af en leverde een bewijs van concept voor het streven.

Door Hitesh Choudhary

Run-time compatibiliteit (meestal Python)

Op dit moment was de kern van Ray ooit werken, konden we ons concentreren op het porten van code op een hoger niveau. Sommige problemen waren vrij eenvoudig op te lossen – zo hebben sommige Python-APIs die alleen UNIX zijn (bijv. os.uname()[1]) vaak geschikte vervangingen op Windows (zoals socket.gethostname()), en het vinden ervan was een kwestie van weten dat je naar alle exemplaren ervan in de codebase moest zoeken. Andere problemen waren moeilijker op te sporen of op te lossen. Soms waren ze te wijten aan het gebruik van POSIX-specifieke commandos (zoals ps), die alternatieve benaderingen vereisten (zoals het gebruik van psutil voor Python). Andere keren waren ze te wijten aan incompatibiliteiten in bibliotheken van derden. Wanneer een stopcontact bijvoorbeeld wordt losgekoppeld in Windows, wordt er een fout gegenereerd in plaats van dat dit resulteert in lege leesbewerkingen. De Python-bibliotheek voor Redis leek dit niet aan te kunnen. Dergelijke verschillen in gedrag vereisten expliciete monkey-patching om af en toe verwarrende fouten te voorkomen die zouden optreden bij het beëindigen van Ray.

Hoewel sommige van dergelijke problemen zijn tamelijk vervelend maar waarschijnlijk verwacht (zoals het vervangen van het gebruik van /tmp door de tijdelijke directory van het platform, of het vermijden van de veronderstelling dat alle absolute paden beginnen met een schuine streep), sommige waren enigszins onverwacht (zoals conflicterende poortreserveringen ) of (zoals vaak het geval is) vanwege verkeerde aannames, en het begrijpen ervan hangt af van het begrijpen van de architectuur van Windows en zijn benaderingen van achterwaartse compatibiliteit.

Een van die verhalen draait om het gebruik van slashes als directoryscheidingstekens in Windows. Over het algemeen lijken deze prima te werken en worden ze vaak gebruikt door ontwikkelaars. Dit is echter in feite te wijten aan de automatische conversie van slashes naar backslashes in de gebruikersmodus Windows-subsysteembibliotheken, en bepaalde automatische verwerking kan worden onderdrukt door paden expliciet te laten voorafgaan met een \\?\ voorvoegsel, wat handig is om bepaalde compatibiliteitsfuncties (zoals lange paden) te omzeilen. We gebruikten echter nooit expliciet een dergelijk pad en gingen ervan uit dat gebruikers ongebruikelijk gebruik in onze experimentele releases zouden vermijden. Het werd echter later duidelijk dat wanneer Bazel bepaalde Python-tests aanriep, paden in dit formaat zouden worden verwerkt om het gebruik van lange paden mogelijk te maken, en dit schakelde de automatische vertaling uit waarop we impliciet vertrouwden. Dit leidde ons tot belangrijke opmerkingen: ten eerste dat het over het algemeen beter is om APIs te gebruiken op de meest geschikte manier voor het doelsysteem, aangezien dit de minste kans biedt op onverwachte problemen .Ten tweede, en het belangrijkste, is het gewoon een misvatting om aan te nemen dat de omgeving van een gebruiker voorspelbaar is . De realiteit is dat moderne software bijna altijd afhankelijk is van het uitvoeren van code van derden waarvan we ons niet bewust zijn van het precieze gedrag. Zelfs wanneer kan worden aangenomen dat een gebruiker problematische situaties vermijdt, is software van derden zich totaal niet bewust van dergelijke verborgen veronderstellingen. Het is dus waarschijnlijk dat ze hoe dan ook zullen voorkomen, niet alleen bij gebruikers, maar ook bij de softwareontwikkelaars zelf, wat resulteert in bugs die moeilijker op te sporen dan op te lossen zijn bij het schrijven van de oorspronkelijke code. Daarom is het belangrijk om bij het ontwerpen van een robuust systeem niet te veel nadruk te leggen op programmavriendelijkheid (het tegenovergestelde van gebruiksvriendelijkheid).

(Terzijde: in Windows, paden kan in feite aanhalingstekens en vele andere speciale tekens bevatten waarvan normaal gesproken wordt aangenomen dat ze illegaal zijn. Dit gebeurt wanneer alternatieve NTFS-gegevensstromen worden gebruikt. Deze zijn echter zeldzaam en complex genoeg dat zelfs standaardtaalbibliotheken ze vaak niet verwerken.)

Toen de meeste significante problemen eenmaal waren opgelost, konden veel tests Windows doorgeven, waardoor de eerste experimentele Windows-implementatie van Ray ontstond.

Door Ross Sneddon

Runtime-verbeteringen (bijv. Unicode-ondersteuning)

Op dit punt kan veel van de kern van Ray op Windows worden gebruikt, net als op andere platforms. Niettemin blijven er enkele problemen bestaan, die voortdurende inspanningen vergen om ze aan te pakken.

Unicode-ondersteuning is zon probleem. Om historische redenen heeft het Windows-gebruikersmodus-subsysteem twee versies van de meeste APIs: een “ANSI” -versie die enkelbyte-tekensets ondersteunt, en een “Unicode” -versie die UCS-2 of UTF-16 ondersteunt (afhankelijk van de bijzonderheden van de betreffende API). Helaas is geen van beide UTF-8; zelfs basisondersteuning voor Unicode vereist het gebruik van tekenreeksen met brede tekens (gebaseerd op wchar_t) over de hele codebase. (NB: Microsoft heeft onlangs geprobeerd UTF-8 als codepagina te introduceren, maar het wordt niet goed genoeg ondersteund om dit probleem naadloos aan te pakken, althans zonder te vertrouwen op mogelijk ongedocumenteerde en kwetsbare Windows-internals.)

Traditioneel hanteren Windows-programmas Unicode door het gebruik van macros zoals _T() of TEXT(), die kunnen worden uitgebreid naar smalle of brede letterlijke tekens, afhankelijk van of een Unicode-build is opgegeven, en gebruik TCHAR als hun generieke tekentypes. Evenzo hebben de meeste C-APIs TCHAR -afhankelijke versies (zoals _tcslen() in plaats van strlen()) om compatibiliteit met beide typen code mogelijk te maken. Het migreren van een op UNIX gebaseerde codebase naar dit model is echter een nogal ingewikkelde onderneming. Dit is nog niet gedaan in Ray, en daarom ondersteunt Ray op het moment van schrijven geen correcte Unicode in (bijvoorbeeld) bestandspaden op Windows, en de beste manier om dit te doen is misschien nog een beetje een open vraag. / p>

Een ander dergelijk probleem is het communicatiemechanisme tussen processen. Hoewel TCP-sockets prima kunnen werken op Windows, zijn ze niet optimaal, aangezien ze een laag van onnodige complexiteit in de logica introduceren (zoals time-outs, keep-alives, Nagles algoritme), kunnen resulteren in onbedoelde bereikbaarheid vanaf niet-lokale hosts, en introduceren wat performance overhead. In de toekomst zouden Named Pipes een betere vervanging kunnen zijn voor UNIX-domeinsockets op Windows; in feite, zelfs onder Linux, kunnen ofwel pipes ofwel de zogenaamde abstract UNIX-domeinsockets ook betere alternatieven blijken te zijn, omdat ze niet vereisen dat socketbestanden op het bestandssysteem worden opgeruimd en opgeruimd.

Ten slotte is een ander voorbeeld van een dergelijk probleem de compatibiliteit met BSD-sockets, of liever het ontbreken ervan. Een uitstekend antwoord op StackOverflow bespreekt enkele van de problemen diepgaand, maar in het kort, terwijl gewone socket-APIs afgeleiden zijn van de originele BSD-socket-API, implementeren verschillende platforms vergelijkbare socket vlaggen anders. Met name conflicten met bestaande IP-adressen of TCP-poorten kunnen leiden tot verschillend gedrag op verschillende platforms. Hoewel de problemen hier moeilijk in detail te beschrijven zijn, is het eindresultaat dat dit het moeilijk kan maken om meerdere exemplaren van Ray tegelijkertijd op dezelfde host te gebruiken. (In feite, aangezien dit afhangt van het OS-kernelgedrag, heeft het ook invloed op WSL.) Dit is nog een ander bekend probleem waarvan de oplossing nogal ingewikkeld is en niet volledig wordt aangepakt in het huidige systeem.

Conclusie

Het proces van het porten van een codebase zoals die van Ray naar Windows is een waardevolle ervaring geweest die de voor- en nadelen van vele aspecten van softwareontwikkeling en hun impact op code-onderhoud belicht.De voorgaande beschrijving belicht slechts enkele van de obstakels die je onderweg tegenkomt. Er kunnen veel nuttige conclusies worden getrokken uit het proces, waarvan sommige waardevol kunnen zijn om hier te delen voor andere projecten die een soortgelijk doel hopen te bereiken.

Ten eerste, in sommige gevallen, ontdekten we later in de daaropvolgende versies van sommige bibliotheken (zoals hiredis) hadden al enkele problemen opgelost die we hadden aangepakt. De oplossingen waren niet altijd voor de hand liggend, aangezien (bijvoorbeeld) de versie van hiredis in recente Redis-versies eigenlijk een verouderde kopie van hiredis was, waardoor we dachten dat sommige problemen nog niet waren opgelost. Ook hebben latere herzieningen niet altijd alle bestaande compatibiliteitsproblemen volledig aangepakt. Desalniettemin zou het mogelijk een beetje moeite hebben bespaard om dieper te kijken naar bestaande oplossingen voor sommige problemen om te voorkomen dat ze opnieuw moesten worden opgelost.

Door John Barkiple

Ten tweede, de toeleveringsketen van software is vaak complex . Bugs kunnen van nature op elke laag samenkomen, en het is een misvatting om te vertrouwen dat zelfs veelgebruikte open-source-tools beproefd en daarom robuust zijn, vooral wanneer ze worden gebruikt in woede . Bovendien hebben veel al lang bestaande of veelvoorkomende problemen met software-engineering geen bevredigende oplossingen voor gebruik, vooral (maar niet alleen) wanneer ze compatibiliteit tussen verschillende systemen vereisen. Inderdaad, afgezien van louter eigenaardigheden, kwamen we tijdens het porten van Ray naar Windows regelmatig bugs tegen in talloze stukjes software, inclusief maar niet beperkt tot een Git-bug op Linux die het gebruik van Bazel beïnvloedde, Redis (Linux) , glog , psutil (parseerbug die invloed heeft op WSL) , grpc , veel moeilijk te identificeren bugs in Bazel zelf (bijv. 1 , 2 , 3 , 4 ), Travis CI , en GitHub-acties , onder andere. Dit moedigde ons aan om ook meer aandacht te besteden aan de complexiteit van onze afhankelijkheden.

Ten derde, investeren in tooling en infrastructuur keert op de lange termijn dividend uit. Snellere builds zorgen voor snellere ontwikkeling, en krachtigere tools maken het gemakkelijker om complexe problemen op te lossen. In ons geval heeft het gebruik van Bazel ons op veel manieren geholpen, ondanks dat het verre van perfect was en het een steile leercurve oplegde. Wat tijd (mogelijk meerdere dagen) investeren om de mogelijkheden, sterke punten en tekortkomingen van een nieuwe tool te leren, is zelden gemakkelijk, maar is waarschijnlijk gunstig voor het onderhoud van de code. In ons geval hebben we wat tijd besteed aan het grondig lezen van de Bazel-documentatie, waardoor we veel sneller een groot aantal toekomstige problemen en oplossingen konden vinden. Bovendien hielp het ons ook om tooling te integreren met Bazel waar weinig anderen blijkbaar in waren geslaagd, zoals Clangs include-what-you-use tool.

Ten vierde, en zoals eerder vermeld, is het verstandig om veilige coderingspraktijken te gebruiken, zoals het initialiseren van geheugen vóór gebruik wanneer er is geen significante afweging. Zelfs de meest zorgvuldige ingenieur kan de toekomstige evolutie van het onderliggende systeem niet noodzakelijkerwijs voorspellen, waardoor veronderstellingen stilzwijgend kunnen worden ontkracht.

Ten slotte, zoals over het algemeen het geval is in de geneeskunde, preventie is de beste remedie . Door rekening te houden met mogelijke toekomstige ontwikkelingen en te coderen naar gestandaardiseerde interfaces, is codeontwerp meer uitbreidbaar dan gemakkelijk kan worden bereikt nadat er incompatibiliteitsproblemen zijn opgetreden.

Hoewel de overdracht van Ray naar Windows nog niet voltooid is, is het behoorlijk tot nu toe succesvol, en we hopen dat het delen van onze ervaring en oplossingen kan dienen als een nuttige gids voor andere ontwikkelaars die overwegen aan een soortgelijke reis te beginnen.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *