Portarea Ray către Microsoft Windows

De Johny vino

(Mehrdad Niknami) (28 sept. 2020)

Fundal

Când a fost lansat pentru prima dată Ray , acesta a fost scris pentru Linux și macOS – sisteme de operare bazate pe UNIX. Suportul pentru Windows, cel mai popular sistem de operare desktop, lipsea, dar important pentru succesul pe termen lung al proiectului. În timp ce subsistemul Windows pentru Linux (WSL) a furnizat o posibilă opțiune pentru unii utilizatori, acesta a limitat asistența la versiunile recente de Windows 10 și a făcut mult mai dificil pentru cod pentru a interacționa cu sistemul de operare nativ Windows, ceea ce a însemnat o experiență slabă pentru utilizatori. Având în vedere aceste aspecte, am sperat cu adevărat să oferim suport nativ Windows utilizatorilor dacă este practic.

Portarea Ray către Windows, totuși, a fost departe de a fi o sarcină banală. La fel ca multe alte proiecte care încearcă să atingă portabilitatea, am întâmpinat multe provocări ale căror soluții nu erau evidente. În această postare pe blog, ne propunem să analizăm detaliile tehnice ale procesului pe care l-am urmat pentru a face Ray compatibil cu Windows. Sperăm că poate ajuta alte proiecte cu o viziune similară să înțeleagă unele dintre potențialele provocări implicate și modul în care acestea ar putea fi gestionate.

Prezentare generală

Suportul de adaptare pentru Windows a reprezentat un aspect din ce în ce mai mare provocare pe măsură ce dezvoltarea pe Ray a progresat în continuare. Înainte de a începe, ne-am așteptat ca anumite aspecte ale codului să constituie o parte semnificativă a eforturilor noastre de portabilitate, inclusiv următoarele:

  • Comunicarea inter-proces (IPC) și memoria partajată
  • Mânerele de obiecte și mânerele / descriptorii de fișiere (FD)
  • Procesul de reproducere și gestionare, inclusiv semnalizarea
  • Gestionarea fișierelor, gestionarea firelor și I / O asincrone
  • Shell și utilizarea comenzilor de sistem
  • Redis

Având în vedere amploarea problemelor, ne-am dat seama rapid că prevenirea introducerii unor incompatibilități suplimentare ar trebui să fie prioritară față de încercarea de a rezolva existența probleme. Astfel, am încercat să procedăm aproximativ în următorii pași, deși aceasta este oarecum o simplificare, deoarece problemele specifice au fost uneori abordate în diferite etape:

  1. Compilabilitatea pentru dependențele terților
  2. Compilabilitate pentru Ray (prin butoane goale & TODOs)
  3. Legabilitate
  4. Integrare continuă (CI) (pentru a bloca alte modificări incompatibile)
  5. Compatibilitate statică (în principal C ++)
  6. Executabilitate în timp de execuție (POC minim)
  7. Compatibilitate în timp de execuție (în principal Python)
  8. Executare -îmbunătățiri de timp (de exemplu, asistență Unicode)
De Ashkan Forouzani

Procesul de dezvoltare

La un nivel ridicat, abordările de aici pot varia. Pentru proiectele mai mici, poate fi foarte benefic să simplificați baza de cod în același timp în care codul este portat pe o nouă platformă. Cu toate acestea, abordarea pe care am adoptat-o, care s-a dovedit destul de utilă pentru o bază de cod mare și care se schimbă dinamic, a fost de a aborda problemele pe rând , păstrează modificările cât mai ortogonale între ele posibil și prioritizează păstrarea semanticii față de simplitate .

Uneori, acest lucru necesită scrierea unui cod potențial străin pentru a gestiona condițiile care nu s-au produs neapărat în producție (să zicem, citând un calea fișierului care nu ar avea niciodată spații). Alteori, acest lucru a necesitat introducerea unor soluții „mecanice” mai generale, care ar fi putut fi evitate (cum ar fi utilizarea unui std::shared_ptr pentru anumite cazuri în care un design diferit ar putea avea a fost permisă utilizarea unui std::unique_ptr). Cu toate acestea, păstrarea semanticii codului existent a fost absolut crucială pentru a evita introducerea constantă de noi bug-uri în baza de cod, care ar fi afectat restul echipei. Și, în retrospectivă, această abordare a fost destul de reușită: erorile de pe alte platforme rezultate din modificările legate de Windows au fost destul de rare și au apărut cel mai frecvent din cauza modificărilor dependențelor decât a modificărilor semantice în baza de cod.

Compilabilitate (dependențe terțe)

Primul obstacol a fost asigurarea faptului că dependențele terților puteau fi construite pe Windows. În timp ce multe dintre dependențele noastre erau biblioteci utilizate pe scară largă și majoritatea nu aveau incompatibilități majore , acest lucru nu a fost întotdeauna simplu. În special, complexitățile accidentale abundă în mai multe fațete ale problemei.

  • Fișierele de compilare (în special fișierele de construcție Bazel) pentru unele proiecte au fost uneori inadecvate pe Windows, necesitând corecții . De multe ori acestea s-au datorat unor probleme care au apărut mai rar pe platformele UNIX, cum ar fi problema citat căilor de fișiere cu spații, a lansarea a interpretului Python adecvat sau problema legării corespunzătoare a legării bibliotecilor partajate față de bibliotecile statice. Din fericire, unul dintre cele mai utile instrumente pentru rezolvarea acestei probleme a fost Bazel însuși: capacitatea sa integrată de a corela spații de lucru externe este, după cum am aflat repede, extrem de util. A permis o metodă standard de corecție a bibliotecilor externe fără a modifica procesele de construcție într-o manieră ad-hoc, păstrând baza de cod curată.
  • Lanțurile de instrumente de construcție și-au prezentat propriile complexități, deoarece alegerea compilatorului sau a linkerului a fost adesea afectată bibliotecile, API-urile și definițiile pe care s-ar putea baza. Și, din păcate, schimbarea compilatoarelor pe Bazel poate fi destul de greoaie . Pe Windows, în special, cea mai bună experiență vine adesea cu utilizarea instrumentelor de construire Microsoft Visual C ++, dar lanțurile noastre de instrumente de pe alte platforme s-au bazat pe GCC sau Clang. Din fericire, lanțul de instrumente LLVM vine cu Clang-Cl pe Windows, care permite utilizarea unui amestec de funcții Clang și MSVC, ceea ce face multe probleme mult mai ușor de rezolvat.
  • Dependențele bibliotecii au prezentat în general cele mai multe dificultăți. Uneori problema a fost la fel de banală ca un antet lipsă sau o definiție conflictuală care ar putea fi tratată mecanic, la care nici bibliotecile utilizate pe scară largă (cum ar fi Boost) nu erau imune. Soluțiile la acestea au fost adesea câteva definiții macro sau anteturi false. În alte cazuri – cum ar fi pentru bibliotecile hiredis și Arrow – bibliotecile au necesitat funcționalitatea POSIX care nu a fost disponibil pe Windows (cum ar fi semnalele sau posibilitatea de a trece descriptori de fișiere prin socket-uri de domeniu UNIX). Acestea s-au dovedit mult mai dificil de rezolvat în unele cazuri. Întrucât am fost mai îngrijorați de compilabilitate în acest stadiu, totuși, a fost fructuos să amânăm implementarea API-urilor mai complexe într-o etapă ulterioară și să folosim butoane de bază sau să dezactivăm codul ofensator pentru a permite construirea să continue.

Odată ce am terminat compilarea dependențelor, ne-am putea concentra pe compilarea nucleului Ray în sine.

Compilabilitate (Ray)

Următorul obstacol a fost să-l ducem pe Ray în sine compilați, împreună cu magazinul Plasma (parte din Săgeată ). Acest lucru a fost mai dificil, deoarece codul nu a fost deseori conceput deloc pentru modelul Windows și s-a bazat foarte mult pe API-urile POSIX. În unele cazuri, aceasta a fost doar o chestiune de găsire și utilizare a anteturilor corespunzătoare (cum ar fi WinSock2.h în loc de sys/time.h pentru struct timeval, ceea ce poate fi surprinzător) sau crearea unor înlocuitori (, cum ar fi pentru

unistd.h ). În alte cazuri, am dezactivat codul incompatibil și am lăsat TODO-urile pentru a le aborda în viitor.

În timp ce acest lucru era similar din punct de vedere conceptual cu cazul gestionarea dependențelor terților, o preocupare unică la nivel înalt care a apărut aici a fost minimizarea modificărilor aduse bazei de cod , în loc să furnizeze cea mai elegantă soluție posibilă. În special, întrucât echipa a actualizat continuu baza de cod sub presupunerea unei API POSIX și deoarece baza de cod nu a fost încă compilată cu Windows, soluții care erau „drop-in” și care ar putea fi utilizate în mod transparent cu modificări minime pentru a obține o compatibilitate cuprinzătoare între întreaga bază de coduri a fost mult mai utilă în evitarea conflictelor de fuziune sintactică sau semantică decât soluțiile de natură chirurgicală. Datorită acestui fapt, în loc să modificăm fiecare site de apel, am creat propriile noastre echivalente Windows shims pentru API-urile POSIX care au simulat comportamentul dorit, chiar dacă acest lucru a fost suboptim per total. Acest lucru ne-a permis să asigurăm compilabilitatea și să oprim proliferarea modificărilor incompatibile mult mai rapid (și mai târziu să abordăm în mod uniform fiecare problemă individuală din întreaga bază de cod în lot) decât ar fi posibil altfel.

Linkability

Odată realizată compilabilitatea, următoarea problemă a fost legarea corectă a fișierelor obiect pentru a obține binare executabile.Deși teoretic sunt simple, problemele de legare implică adesea multe complexități accidentale, cum ar fi următoarele:

  • Anumite biblioteci de sistem erau specifice platformei și nu erau disponibile sau diferite pe alte platforme ( cum ar fi libpthread)
  • Anumite biblioteci au fost conectate dinamic atunci când se aștepta ca acestea să fie conectate static (sau invers)
  • Anumite definiții ale simbolurilor lipseau sau erau conflictuale (de exemplu, connect din hiredis în conflict cu connect pentru sockets)
  • Anumite dependențe funcționau în mod coincident pe sistemele POSIX, dar au necesitat o manipulare explicită în Bazel pe Windows

Soluțiile pentru acestea au fost adesea modificări banale (deși poate neevidente) ale fișierelor de construcție, deși, în unele cazuri, a fost necesar să se corecte dependențele. La fel ca înainte, abilitatea Bazel de a corela practic orice fișier dintr-o sursă externă a fost extrem de utilă pentru a ajuta la soluționarea oricăror astfel de probleme.

De Richy Great

Obținerea bazei de cod pentru a construi cu succes a fost o etapă majoră. Odată ce modificările specifice platformei au fost integrate, întreaga echipă și-ar asuma acum responsabilitatea pentru asigurarea portabilității statice în toate modificările viitoare și introducerea modificărilor incompatibile ar fi redusă la minimum pe baza de cod. (Portabilitatea Dinamică nu era, desigur, încă posibilă în acest moment, deoarece butoanele nu aveau adesea funcționalitatea necesară pe Windows și, prin urmare, codul nu putea rula în această etapă.)

Obținerea bazei de cod pentru a construi cu succes a fost o etapă majoră. Odată ce modificările specifice platformei au fost integrate, întreaga echipă și-ar asuma acum responsabilitatea pentru asigurarea portabilității statice în toate modificările viitoare și introducerea modificărilor incompatibile ar fi redusă la minimum pe baza de cod. (Portabilitatea Dinamică nu era, desigur, încă posibilă în acest moment, deoarece butoanele nu aveau adesea funcționalitatea necesară pe Windows și, prin urmare, codul nu putea rula în această etapă.)

Acest lucru nu numai că a scăzut volumul de muncă, dar a permis efortul de portabilitate Windows să încetinească și să procedeze mai încet și cu atenție pentru a aborda incompatibilitățile mai dificile în profunzime, fără a fi nevoie să atragă atenția asupra modificărilor de rupere care sunt introduse constant în baza de cod. Acest lucru a permis soluții de calitate superioară pe termen lung, inclusiv o refacturare a bazei de cod care ar putea afecta semantica pe alte platforme.

Compatibilitate statică (în principal C / C ++)

posibil cea mai consumatoare de etapă în care ar fi eliminate sau elaborate butoanele de compatibilitate, codul dezactivat ar putea fi reactivat și problemele individuale ar putea fi abordate. Natura statică a C ++ a permis diagnosticului compilatorului pentru a ghida cele mai multe astfel de modificări necesare fără a fi nevoie să executați niciun cod.

Modificările individuale de aici, inclusiv unele menționate anterior, ar fi prea lungi pentru a fi discutate în profunzime. în întregime și anumite probleme individuale ar fi probabil demne de propriile lor postări lungi pe blog. De exemplu, reproducerea și gestionarea procesului este de fapt un efort surprinzător de complex, plin de idiosincrazii și capcane (vezi aici , de exemplu) pentru care pare să nu existe nimic bun, bibliotecă pe mai multe platforme, deși aceasta este o problemă veche. O parte din aceasta se datorează proiectărilor inițiale slabe din API-urile sistemului de operare. ( Codul sursă Chromium oferă o privire asupra unora dintre complexitățile care sunt adesea ignorate în altă parte. Într-adevăr, codul sursă Chromium poate servi adesea ca o ilustrare excelentă a manipulării multe incompatibilități de platformă și subtilități.) Ignorarea noastră inițială pentru acest fapt ne-a determinat să încercăm să folosim Boost.Process, dar lipsa unei semantice clare de proprietate pentru POS div pid_t identificarea procesului și proprietatea), precum și erori în biblioteca Boost.Process în sine au dus la erori dificil de găsit în baza de cod, rezultând în cele din urmă decizia noastră de a reveni la această modificare și de a introduce propria noastră abstractizare. Mai mult, biblioteca Boost.Process a fost destul de grea chiar și pentru o bibliotecă Boost, iar acest lucru a încetinit versiunile noastre considerabil. În schimb, am ajuns să scriem propriul nostru wrapper pentru obiecte de proces. Acest lucru sa dovedit a funcționa destul de bine pentru scopurile noastre. Una dintre soluțiile noastre în acest caz a fost să să luăm în considerare adaptarea soluțiilor la propriile nevoi și să nu presupunem că soluțiile preexistente sunt cea mai bună alegere .

Aceasta a fost, desigur, doar o privire asupra problemei gestionării proceselor.Unele alte aspecte ale efortului de portabilitate merită, de asemenea, elaborate.

De Thomas Jensen

Porțiuni din baza de cod (cum ar fi comunicarea cu serverul Plasma din Săgeată) asumate posibilitatea de a utiliza socket-uri de domeniu UNIX (AF_UNIX) pe Windows. În timp ce versiunile recente de Windows 10 nu acceptă socket-urile din domeniul UNIX, implementările nu sunt suficiente pentru acoperirea tuturor utilizărilor posibile ale domeniului UNIX sockets, nici socket-urile din domeniul UNIX nu sunt deosebit de elegante: necesită curățare manuală și pot lăsa fișiere inutile pe sistemul de fișiere în cazul unei terminări ne-curate a unui proces și nu oferă posibilitatea de a trimite date auxiliare (cum ar fi descriptori de fișiere) la un alt proces. Deoarece Ray folosește Boost.Asio, cel mai oportun înlocuitor pentru socket-urile de domeniu UNIX au fost socket-urile TCP locale (ambele putând fi abstractizate ca socketuri generale), așa că am trecut la acestea din urmă, doar a căilor de fișiere cu versiuni cu șiruri de adrese TCP / IP fără a fi nevoie să refactorizați baza de cod.

Acest lucru nu a fost suficient, deoarece utilizarea soclurilor TCP încă nu a oferit posibilitatea de a reproduce descriptori de socket în alte procese. Într-adevăr, o soluție adecvată la această problemă ar fi fost să o evităm cu totul. Întrucât acest lucru ar necesita o refactorizare pe termen mai lung a părților relevante ale bazei de cod (o sarcină pe care alții au început-o), totuși, o abordare transparentă a apărut mai adecvată între timp. Acest lucru a fost îngreunat de faptul că, pe sistemele bazate pe UNIX, duplicarea unui descriptor de fișier nu necesită cunoașterea identității procesului țintă, în timp ce, pe Windows, duplicarea unui handle necesită manipularea activă a procesul țintă. Pentru a rezolva aceste probleme, am implementat capacitatea de a schimba descriptorii de fișiere prin înlocuirea procedurii de strângere a mâinii în lansarea magazinului Plasma cu un mecanism

mai specializat care ar stabili o conexiune TCP , căutați ID-ul procesului la celălalt capăt (o procedură poate lentă, dar o singură dată), copiați mânerul socket și informați procesul țintă despre noul handle. Deși aceasta nu este o soluție cu scop general (și, de fapt, poate fi predispusă la condițiile de rasă în cazul general) și o abordare destul de neobișnuită, a funcționat bine în scopul Ray și poate fi o abordare a altor proiecte care se confruntă cu aceeași problemă. ar putea beneficia de acest lucru.

Dincolo de aceasta, una dintre cele mai mari probleme la care ne așteptam să ne confruntăm a fost dependența de server Redis . În timp ce Microsoft Open Technologies (MSOpenTech) a implementat anterior un port Redis în Windows, proiectul a fost abandonat și, în consecință, nu a acceptat versiunile de Redis pe care Ray le cerea . Acest lucru ne-a determinat inițial să presupunem că va trebui să rulăm în continuare serverul Redis pe subsistemul Windows pentru Linux (WSL), ceea ce probabil s-ar fi dovedit incomod pentru utilizatori. Atunci am fost destul de recunoscători că am descoperit că un alt dezvoltator a continuat proiectul pentru a produce mai târziu binarele Redis pe Windows (vezi tporadowski / redis ). Acest lucru ne-a simplificat enorm problema și ne-a permis să oferim suport nativ pentru Ray pentru Windows.

În cele din urmă, probabil cele mai semnificative obstacole cu care ne-am confruntat (la fel ca MSOpenTech Redis și la fel ca majoritatea celorlalte programe numai POSIX) a fost absența unor supleanți banali pentru unele API-uri POSIX pe Windows. Unele dintre acestea (, cum ar fi

getppid()) au fost simple, deși oarecum plictisitoare. Poate că cea mai dificilă problemă întâlnită pe parcursul întregului proces de portare a fost totuși cea a descriptorilor de fișiere vs. O mare parte din codul pe care ne-am bazat (cum ar fi magazinul de plasmă din Săgeată) a presupus utilizarea descriptorilor de fișiere POSIX (int s). Cu toate acestea, Windows folosește în mod nativ HANDLE s, care sunt de dimensiunea indicatorului și analog cu size_t. În sine, însă, aceasta nu este o problemă semnificativă, deoarece Microsoft Visual C ++ runtime (CRT) oferă un strat POSIX. Cu toate acestea, funcționalitatea stratului este limitată, necesită traduceri pe fiecare site de apeluri care nu o acceptă și, în special, nu poate fi utilizată pentru lucruri precum socketuri sau mânere de memorie partajată. Mai mult, nu am vrut să presupunem că HANDLE s ar fi întotdeauna suficient de mici pentru a se încadra într-un număr întreg de 32 de biți, chiar dacă acest lucru a fost adesea cazul, deoarece nu era clar dacă circumstanțele de care nu eram conștienți ar putea încălca această presupunere în tăcere.Acest lucru a agravat problemele noastre în mod semnificativ, deoarece soluția cea mai evidentă ar fi fost să detectăm toate int s care reprezintă descriptorii de fișiere dintr-o bibliotecă precum Arrow și să le înlocuim (și toate utilizările lor) ) cu un tip de date alternativ, care a fost un proces predispus la erori și a implicat corecții semnificative pentru codul extern, creând o sarcină semnificativă de întreținere.

A fost destul de dificil să decidem ce să facem în această etapă. Soluția a MSOpenTech Redis pentru aceeași problemă a arătat clar că aceasta a fost o sarcină destul de descurajantă, deoarece au rezolvat această problemă prin crearea unui tabel descriptor de fișiere unic, la nivel de proces, pe partea de sus a implementării CRT existente, care le cere să se ocupe de siguranța firelor, precum și forțându-i să intercepteze toate utilizează API-urile POSIX (chiar și cele pe care era deja capabil să le gestioneze) doar pentru a traduce descriptorii de fișiere. În schimb, am decis să adoptăm o abordare neobișnuită: am extins stratul de traducere POSIX din CRT . Acest lucru a fost realizat prin identificarea mânerelor incompatibile la momentul creării și „împingerea” acestor mânere în tampoanele unei conducte de prisos, returnând în schimb descriptorul acelei conducte. Apoi a trebuit doar să modificăm site-urile de utilizare ale acestor mânere, care erau, în mod esențial, banale de identificat, deoarece toate erau soclu și fișier mapat cu memorie . De fapt, acest lucru a contribuit la evitarea nevoii de corecție, deoarece am reușit doar să redirecționăm multe funcții prin macro .

În timp ce efortul de a dezvoltarea acestui strat unic de extensie (în win32fd.h) a fost semnificativ (și destul de neortodox), probabil că a meritat, deoarece stratul de traducere a fost, de fapt, destul de mic în comparație și ne-a permis să delegăm cele mai multe probleme fără legătură (cum ar fi blocarea multithread a tabelului descriptor de fișiere) către API-urile CRT. Mai mult, prin folosind conductele anonime din același tabel global de descriptor de fișiere (în ciuda lipsei accesului nostru direct la acesta), am putut evita să interceptăm și să traducem descriptori de fișiere pentru alte funcții care ar putea fi deja gestionate direct. Acest lucru a permis ca o mare parte a codului să rămână esențial neschimbată la un impact minim asupra performanței, până când mai târziu am avut șansa de a refactoriza codul și de a oferi împachetări mai bune la un nivel superior (cum ar fi prin intermediul împachetărilor Boost.Asio). Este foarte posibil ca o extensie a acestui strat să permită altor proiecte, cum ar fi Redis, să fie portate pe Windows mult mai ușor și cu modificări mult mai puțin drastice sau potențial de erori.

De Andrea Leopardi

Executabilitate la executare (Proof-of-Concept)

Odată ce am crezut că nucleul Ray funcționează corect, următoarea etapă a fost să ne asigurăm că un test Python ne-ar putea exercita cu succes căile de cod. Inițial, nu am acordat prioritate acestui lucru. Cu toate acestea, acest lucru s-a dovedit a fi o greșeală, deoarece modificările ulterioare ale altor dezvoltatori din echipă au introdus de fapt incompatibilități mai dinamice cu Windows, iar sistemul CI nu a putut detecta astfel de defecțiuni. Prin urmare, am făcut ulterior o prioritate să efectuăm un test minim pe versiunile Windows, pentru a evita ruperea ulterioară a versiunii.

Pentru în cea mai mare parte, eforturile noastre au avut succes și orice erori persistente din nucleul Ray se aflau în părți predictibile ale bazei de cod (deși abordarea lor a necesitat adesea trecerea prin codul multi-proces, care era departe de a fi banal). Cu toate acestea, a existat cel puțin o surpriză oarecum neplăcută pe parcurs pe partea C și una pe partea Python, ambele (printre altele) ne încurajând să citim documentația software mai proactiv în viitor.

Pe partea C, strângerea de mână inițială wrapper pentru schimbul mânerelor de soclu s-a bazat pe înlocuirea naivă a sendmsg și recvmsg cu WSASendMsg și WSARecvMsg. Aceste API-uri Windows erau cele mai apropiate echivalente ale API-urilor POSIX și, astfel, păreau a fi o alegere evidentă. Cu toate acestea, la executare, codul se prăbușea în mod constant, iar sursa problemei era neclară. Unele depanări (inclusiv cu versiunile de depanare ale versiunilor & runtimes) au ajutat la dezvăluirea faptului că problema se referea la variabilele stivei trecute la WSASendMsg. Depanarea ulterioară și inspecția atentă a conținutului memoriei au sugerat că problema ar fi putut fi câmpul msg_flags din WSAMSG, deoarece acesta a fost singurul neinițializat camp.Cu toate acestea, acest lucru pare a fi irelevant: msg_flags a fost tradus doar din flags în struct msghdr, care nu a fost folosit la intrare și a fost folosit doar ca parametru de ieșire. Citind documentația, totuși, a dezvăluit problema: pe Windows, câmpul a servit și ca parametru input și, astfel, lăsându-l neinițializat a rezultat un comportament imprevizibil! Acest lucru a fost destul de neașteptat pentru noi și a dus la două acțiuni importante pentru a merge mai departe: pentru a citi cu atenție documentația fiecărei funcții și în plus, faptul că inițializarea variabilelor nu se referă doar la asigurarea corectitudinii cu API-urile actuale, ci este, de asemenea, important pentru a face codul robust pentru modificările viitoare ale API-urilor vizate .

Pe partea Python, am întâmpinat o problemă diferită. Modulele noastre native Python nu ar putea fi încărcate inițial, în ciuda lipsei unor probleme evidente. După mai multe zile de presupuneri, trecând prin asamblare și codul sursă CPython și inspectând variabilele din baza de cod CPython, a devenit evident că problema a fost lipsa unui .pyd sufix pe Python dinamic. biblioteci pe Windows. Se pare că, din motive neclare pentru noi, Python refuză să încarce chiar și fișiere .dll pe Windows ca module Python, în ciuda faptului că bibliotecile partajate native ar putea fi încărcate în mod normal chiar și cu orice fișier extensie. Într-adevăr, s-a dovedit că acest fapt a fost documentat pe site-ul web Python . Din păcate, totuși, prezența unei astfel de documentații nu ar putea implica realizarea noastră de a o căuta.

Cu toate acestea, în cele din urmă, Ray a reușit să ruleze cu succes pe Windows, iar acest lucru a încheiat următoarea etapă și a oferit o dovadă de concept pentru efort.

De Hitesh Choudhary

Compatibilitate în timpul rulării (în general Python)

În acest moment, odată ce nucleul Ray a fost funcționând, ne-am putut concentra pe portarea codului de nivel superior. Unele probleme erau destul de ușor de soluționat – de exemplu, unele API-uri Python care sunt doar UNIX (de ex., os.uname()[1]) au adesea înlocuiri adecvate pe Windows (cum ar fi ), și găsirea lor a fost o chestiune de a ști să căutați toate instanțele acestora în baza de cod. Alte probleme erau mai greu de depistat sau de rezolvat. Uneori acestea s-au datorat utilizării comenzilor specifice POSIX (cum ar fi ps), care necesitau abordări alternative (cum ar fi utilizarea psutil pentru Python). Alteori, acestea s-au datorat incompatibilităților din bibliotecile terților. De exemplu, atunci când un socket se deconectează pe Windows, se generează o eroare, mai degrabă decât să conducă la citiri goale. Biblioteca Python pentru Redis nu pare să gestioneze acest lucru. Astfel de diferențe de comportament au necesitat corecție de maimuță pentru a evita erorile confuze ocazionale care ar apărea la terminarea Ray.

În timp ce unele astfel de probleme sunt destul de plictisitor, dar probabil așteptat (cum ar fi înlocuirea utilizărilor /tmp cu directorul temporar al platformei sau evitarea presupunerii că toate căile absolute încep cu o bară), unele au fost oarecum neașteptate (cum ar fi rezervări de porturi conflictuale ) sau (așa cum se întâmplă adesea) din cauza ipotezelor greșite și înțelegerea acestora depinde de înțelegerea arhitecturii Windows și a abordărilor sale de compatibilitate inversă.

O astfel de poveste se învârte în jurul utilizării barelor oblice ca separatoare de directoare pe Windows. În general, acestea par să funcționeze bine și sunt utilizate în mod obișnuit de dezvoltatori. Cu toate acestea, acest lucru se datorează de fapt conversiei automate a barelor oblice în barele oblice în bibliotecile subsistemului Windows în modul utilizator și anumite procesări automate pot fi suprimate prin prefixarea explicită a căilor cu un prefix \\?\, ceea ce este util pentru ocolirea anumitor caracteristici de compatibilitate (cum ar fi căile lungi). Cu toate acestea, nu am folosit niciodată în mod explicit o astfel de cale și am presupus că utilizatorii ar putea fi de așteptat să evite utilizarea neobișnuită în versiunile noastre experimentale. Cu toate acestea, a devenit mai târziu evident că atunci când Bazel a invocat anumite teste Python, căile ar fi procesate în acest format pentru a permite utilizarea căilor lungi, iar acest lucru a dezactivat traducerea automată pe care ne-am bazat implicit. Acest lucru ne-a condus la plăți importante: mai întâi, că este, în general, mai bine să utilizați API-urile în modul cel mai potrivit pentru sistemul țintă, deoarece oferă cele mai puține oportunități pentru apariția problemelor neașteptate .În al doilea rând și cel mai important, este pur și simplu o eroare să presupunem că mediul unui utilizator este previzibil . Realitatea este că software-ul modern se bazează aproape întotdeauna pe rularea codului terților ale căror comportamente precise nu le cunoaștem. Chiar și atunci când se poate presupune că un utilizator evită situațiile problematice, software-ul terților nu cunoaște complet aceste ipoteze ascunse. Astfel, este probabil ca acestea să apară oricum, nu doar pentru utilizatori, ci și pentru dezvoltatorii de software înșiși, rezultând erori care sunt mai greu de depistat decât de abordat la scrierea codului inițial. Prin urmare, este important să evitați să puneți prea mult în greutate programul (opusul „ușurinței utilizatorului”) atunci când proiectați un sistem robust.

(Ca o distracție deoparte: de fapt, pe Windows, căile poate conține de fapt ghilimele și multe alte caractere speciale despre care se presupune în mod normal că sunt ilegale. Acest lucru se întâmplă atunci când se utilizează fluxuri de date alternative NTFS. Cu toate acestea, acestea sunt suficient de rare și complexe încât chiar și bibliotecile de limbă standard nu le gestionează.)

Odată ce au fost rezolvate cele mai importante probleme, totuși, multe teste au reușit să treacă pe Windows, creând prima implementare experimentală Windows a Ray.

De Ross Sneddon

Îmbunătățiri în timpul rulării (de exemplu, asistență Unicode)

În acest moment, o mare parte din nucleul Ray poate fi utilizat pe Windows la fel ca pe alte platforme. Cu toate acestea, unele probleme rămân, care necesită eforturi continue pentru a le rezolva.

Asistența Unicode este una dintre aceste probleme. Din motive istorice, subsistemul Windows pentru modul utilizator are două versiuni ale majorității API-urilor: o versiune „ANSI” care acceptă seturi de caractere cu un singur octet și o versiune „Unicode” care acceptă UCS-2 sau UTF-16 (în funcție de detalii ale API-ului în cauză). Din păcate, niciuna dintre acestea nu este UTF-8; chiar și suportul de bază pentru Unicode necesită utilizarea șirurilor de caractere largi (bazate pe wchar_t) pe întreaga bază de cod. (nb: De fapt, Microsoft a încercat recent să introducă UTF-8 ca pagină de cod, dar nu este suficient de bine acceptat pentru a aborda această problemă fără probleme, cel puțin fără să se bazeze pe sistemele interne de Windows potențial nedocumentate și fragile.)

În mod tradițional, programele Windows gestionează Unicode prin utilizarea de macrocomenzi precum _T() sau TEXT(), care se extind la îngust sau larg -literale de caracter în funcție de specificarea unei construcții Unicode și folosiți TCHAR ca tipuri de caractere generice. În mod similar, majoritatea API-urilor C au TCHAR versiuni dependente (cum ar fi _tcslen() în loc de strlen()) pentru a permite compatibilitatea cu ambele tipuri de cod. Cu toate acestea, migrarea unei baze de cod bazate pe UNIX la acest model este un efort destul de implicat. Acest lucru nu s-a făcut încă în Ray și, prin urmare, în acest moment, Ray nu acceptă Unicode corect în (de exemplu) căile de fișiere de pe Windows, iar cea mai bună abordare pentru a face acest lucru poate fi oarecum o întrebare deschisă.

O altă problemă este mecanismul de comunicare între procese. În timp ce soclurile TCP pot funcționa bine pe Windows, acestea sunt suboptimale, deoarece introduc un strat de complexitate inutilă în logică (cum ar fi timeout-urile, keep-alives, algoritmul lui Nagle), pot duce la accesibilitate accidentală de la gazde non-locale și pot introduceți unele performanțe generale. În viitor, Named Pipes ar putea oferi o înlocuire mai bună pentru socket-urile din domeniul UNIX pe Windows; de fapt, chiar și pe Linux, fie țevile, fie așa-numitele abstract socketurile de domeniu UNIX se pot dovedi a fi și alternative mai bune, deoarece nu necesită aglomerarea și curățarea fișierelor de socket-uri pe sistemul de fișiere.

În sfârșit, un alt exemplu de astfel de probleme este compatibilitatea socket-urilor BSD sau, mai degrabă, lipsa acesteia. Un răspuns excelent pe StackOverflow discută în detaliu unele dintre probleme, dar pe scurt, în timp ce API-urile socket comune sunt derivate ale API-ului socket BSD original, diferite platforme implementează socket similar steaguri diferit. În special, conflictele cu adresele IP existente sau porturile TCP pot produce comportamente diferite între platforme. Deși problemele sunt dificil de detaliat aici, rezultatul final este că acest lucru poate face dificilă utilizarea simultană a mai multor instanțe de Ray pe aceeași gazdă. (De fapt, deoarece acest lucru depinde de comportamentul nucleului sistemului de operare, afectează și WSL.) Aceasta este încă o problemă cunoscută a cărei soluție este mai degrabă implicată și nu este complet abordată în sistemul actual.

Concluzie

Procesul de portare a unei baze de cod precum Ray în Windows a fost o experiență valoroasă care evidențiază avantajele și dezavantajele multor aspecte ale dezvoltării software-ului și impactul acestora asupra întreținerii codului.Descrierea precedentă evidențiază doar câteva dintre obstacolele întâmpinate pe parcurs. Multe concluzii utile pot fi extrase din proces, dintre care unele pot fi valoroase de împărtășit aici pentru alte proiecte care speră să atingă un obiectiv similar.

În primul rând, în unele cazuri, am găsit mai târziu versiunile ulterioare. din unele biblioteci (cum ar fi hiredis) au rezolvat deja unele probleme pe care le-am abordat. Soluțiile nu au fost întotdeauna evidente, deoarece (de exemplu) versiunea hiredis din versiunile recente Redis a fost de fapt o copie veche a hiredis, ceea ce ne-a făcut să credem că unele probleme nu au fost încă abordate. Nici reviziile ulterioare nu au abordat întotdeauna pe deplin toate problemele de compatibilitate existente. Cu toate acestea, s-ar fi economisit puțin efortul de a verifica mai adânc soluțiile existente pentru unele probleme, pentru a evita să le rezolvați din nou.

De John Barkiple

În al doilea rând, lanțul de aprovizionare cu software este adesea complex . Bug-urile se pot compune în mod natural la fiecare strat și este o eroare să ai încredere în instrumentele open-source utilizate pe scară largă pentru a fi „testate în luptă” și, prin urmare, robuste, mai ales atunci când sunt utilizate în mânie . Mai mult, multe probleme de inginerie software de lungă durată sau comune nu au soluții satisfăcătoare disponibile pentru utilizare, mai ales (dar nu numai) atunci când necesită compatibilitate între diferite sisteme. Într-adevăr, în afară de simple ciudățenii, în procesul de portare a Ray pe Windows, am întâlnit în mod regulat și am raportat deseori erori în numeroase bucăți de software, inclusiv, dar fără a se limita la, o eroare Git pe Linux care a afectat utilizarea Bazel, Redis (Linux) , glog , psutil (eroare de analiză care afectează WSL) , grpc , multe erori dificil de identificat în Bazel însuși (de exemplu, 1 , 2 , 3 , 4 ), Travis CI și Acțiuni GitHub , printre altele. Acest lucru ne-a încurajat să acordăm o atenție sporită și complexității dependențelor noastre.

În al treilea rând, investind în instrumente și infrastructură plătește dividende pe termen lung. Construcțiile mai rapide permit o dezvoltare mai rapidă, iar instrumentele mai puternice permit rezolvarea mai ușoară a problemelor complexe. În cazul nostru, utilizarea Bazel ne-a ajutat în multe feluri, în ciuda faptului că este departe de a fi perfectă și a impus o curbă de învățare abruptă. Investirea unui timp (posibil mai multe zile) pentru a afla capacitățile, punctele forte și neajunsurile unui nou instrument este rareori ușoară, dar probabil să fie benefică în întreținerea codului. În cazul nostru, petrecerea unui timp citind în detaliu documentația Bazel ne-a permis să identificăm mult mai rapid o multitudine de probleme și soluții viitoare. Mai mult, ne-a ajutat și să integrăm instrumentele cu Bazel pe care puțini alții au reușit să le facă, cum ar fi instrumentul Clang include-what-you-use .

În al patrulea rând, și așa cum s-a menționat anterior, este prudent să vă angajați în practici de codare sigure , cum ar fi inițializarea memoriei înainte de utilizare atunci când nu există un compromis semnificativ. Chiar și cel mai atent inginer nu poate prezice neapărat evoluția viitoare a sistemului de bază care poate invalida în tăcere ipotezele.

În cele din urmă, așa cum se întâmplă în general în medicină, prevenirea este cel mai bun leac . Contabilitatea posibilelor dezvoltări viitoare și codificarea pe interfețe standardizate, permite o proiectare a codului mai extensibilă decât se poate realiza cu ușurință după apariția incompatibilităților.

În timp ce portul Ray către Windows nu este încă complet, a fost destul de succes până acum și sperăm că împărtășirea experienței și soluțiilor noastre poate servi drept ghid util pentru alți dezvoltatori care intenționează să se angajeze într-o călătorie similară.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *