Porting Ray su Microsoft Windows

Di Johny vino

(Mehrdad Niknami) (28 settembre 2020)

Background

Quando Ray è stato lanciato per la prima volta, è stato scritto per Linux e macOS – sistemi operativi basati su UNIX. Il supporto per Windows, il sistema operativo desktop più popolare, mancava, ma era importante per il successo a lungo termine del progetto. Sebbene il Windows Subsystem for Linux (WSL) fornisse una possibile opzione per alcuni utenti, limitava il supporto alle versioni recenti di Windows 10 e rendeva molto più difficile il codice interagire con il sistema operativo Windows nativo, il che rappresentava una scarsa esperienza per gli utenti. Detto questo, speravamo davvero di fornire il supporto nativo di Windows agli utenti, se possibile.

Il porting di Ray su Windows, tuttavia, era tuttaltro che un compito banale. Come molti altri progetti che tentano di raggiungere la portabilità dopo il fatto, abbiamo incontrato molte sfide le cui soluzioni non erano ovvie. In questo post del blog, ci proponiamo di approfondire i dettagli tecnici del processo che abbiamo seguito per rendere Ray compatibile con Windows. Ci auguriamo che possa aiutare altri progetti con una visione simile a comprendere alcune delle potenziali sfide coinvolte e come potrebbero essere gestite.

Panoramica

Il supporto di retrofitting per Windows ha rappresentato un sfida mentre lo sviluppo su Ray progrediva ulteriormente. Prima di iniziare, ci aspettavamo che alcuni aspetti del codice avrebbero costituito una parte significativa dei nostri sforzi di portabilità, inclusi i seguenti:

  • Comunicazione tra processi (IPC) e memoria condivisa
  • Maniglie degli oggetti e FD (file handle / descrittori)
  • Generazione e gestione dei processi, inclusa la segnalazione
  • Gestione dei file, gestione dei thread e I / O asincrono
  • Utilizzo della shell e dei comandi di sistema
  • Redis

Data lentità dei problemi, ci siamo subito resi conto che si dovrebbe dare priorità alla prevenzione dellintroduzione di ulteriori incompatibilità rispetto al tentativo di risolvere problemi. Abbiamo quindi tentato di procedere allincirca con i seguenti passaggi, sebbene questa sia in qualche modo una semplificazione poiché a volte problemi specifici sono stati affrontati in fasi diverse:

  1. Compilabilità per dipendenze di terze parti
  2. Compilabilità per Ray (tramite stub vuoti & TODO)
  3. Collegabilità
  4. Integrazione continua (CI) (per bloccare ulteriori modifiche incompatibili)
  5. Compatibilità statica (principalmente C ++)
  6. Eseguibilità run-time (POC minimo)
  7. Compatibilità run-time (principalmente Python)
  8. Esegui -miglioramenti nel tempo (ad es. supporto Unicode)
Di Ashkan Forouzani

Processo di sviluppo

Ad alto livello, gli approcci qui possono variare. Per progetti più piccoli, può essere molto vantaggioso semplificare la base di codice mentre il codice viene portato su una nuova piattaforma. Tuttavia, lapproccio che abbiamo adottato, che si è rivelato molto utile per un codebase ampio e dinamico, è stato quello di affrontare i problemi uno alla volta , mantiene le modifiche il più ortogonali possibile e dare la priorità alla conservazione della semantica rispetto alla semplicità .

A volte, questo richiedeva la scrittura di codice potenzialmente estraneo per gestire condizioni che potrebbero non essersi verificate necessariamente in produzione (ad esempio, citando un percorso del file che non avrebbe mai spazi). Altre volte, ciò richiedeva lintroduzione di soluzioni “meccaniche” più generiche che avrebbero potuto essere evitate (come lutilizzo di un std::shared_ptr per alcuni casi in cui un design diverso potrebbe avere è stato consentito lutilizzo di std::unique_ptr). Tuttavia, preservare la semantica del codice esistente era assolutamente cruciale per evitare la costante introduzione di nuovi bug nella base di codice, che avrebbero influenzato il resto del team. E, col senno di poi, questo approccio ha avuto un discreto successo: i bug su altre piattaforme risultanti da modifiche relative a Windows erano piuttosto rari e si verificavano più frequentemente a causa di cambiamenti nelle dipendenze che a causa di cambiamenti semantici nella base di codice.

Compilabilità (dipendenze di terze parti)

Il primo ostacolo era garantire che le dipendenze di terze parti potessero essere create su Windows. Sebbene molte delle nostre dipendenze fossero librerie ampiamente utilizzate e la maggior parte non presentava importanti incompatibilità, ciò non era sempre semplice. In particolare, le complessità accidentali abbondavano in diversi aspetti del problema.

  • I file di build (in particolare i file di build di Bazel) per alcuni progetti a volte erano inadeguati su Windows, richiedendo patch . Spesso ciò era dovuto a problemi che si verificavano più raramente su piattaforme UNIX, come il problema di citando i percorsi dei file con spazi, di lanciare linterprete Python appropriato, o il problema di linkare le librerie condivise rispetto alle librerie statiche. Per fortuna, uno degli strumenti più utili per risolvere questo problema è stato lo stesso Bazel: la sua capacità incorporata di applicare patch agli spazi di lavoro esterni è, come abbiamo rapidamente imparato, estremamente utile. Consentiva un metodo standard per applicare patch alle librerie esterne senza modificare i processi di compilazione in modo ad hoc, mantenendo pulito il codebase.
  • Le toolchain di compilazione mostravano le proprie complessità, poiché la scelta del compilatore o del linker spesso influiva le librerie, le API e le definizioni su cui fare affidamento. E sfortunatamente, cambiare compilatori su Bazel può essere abbastanza complicato . Su Windows, in particolare, la migliore esperienza spesso si ottiene utilizzando gli strumenti di compilazione di Microsoft Visual C ++, ma i nostri toolchain su altre piattaforme erano basati su GCC o Clang. Fortunatamente, la toolchain LLVM viene fornita con Clang-Cl su Windows, che consente di utilizzare una combinazione di funzionalità Clang e MSVC, rendendo molti problemi molto più facili da risolvere.
  • Le dipendenze della libreria generalmente presentavano le maggiori difficoltà. A volte il problema era banale come unintestazione mancante o una definizione in conflitto che poteva essere gestita meccanicamente, a cui anche le librerie ampiamente utilizzate (come Boost) non erano immuni. Le soluzioni a questi erano spesso poche definizioni di macro appropriate o intestazioni fittizie. In altri casi, come per le librerie hiredis e Arrow – le librerie richiedevano la funzionalità POSIX che non era disponibile su Windows (come i segnali o la capacità di passare descrittori di file su socket di dominio UNIX). Questi si sono rivelati molto più difficili da risolvere in alcuni casi. Poiché eravamo più preoccupati per la compilabilità in questa fase, tuttavia, è stato utile rimandare limplementazione di API più complesse a una fase successiva e utilizzare stub di base o disabilitare il codice offensivo per consentire alla build di continuare.

Una volta che avessimo finito con la compilazione delle dipendenze, potremmo concentrarci sulla compilazione del nucleo di Ray stesso.

Compilabilità (Ray)

Il prossimo ostacolo è stato far sì che Ray stesso compilare, insieme allarchivio Plasma (parte della Freccia ). Questo era più difficile in quanto il codice spesso non era affatto progettato per il modello Windows e faceva molto affidamento sulle API POSIX. In alcuni casi, si trattava solo di trovare e utilizzare le intestazioni appropriate (ad esempio WinSock2.h al posto di sys/time.h per struct timeval, che potrebbe essere sorprendente) o creazione di sostituti ( come per

unistd.h ). In altri casi, abbiamo disabilitato il codice incompatibile e lasciato TODO da affrontare in futuro.

Sebbene questo fosse concettualmente simile al caso di gestione delle dipendenze di terze parti, un problema di alto livello unico emerso qui era ridurre al minimo le modifiche alla base di codice , invece di fornire soluzione più elegante possibile. In particolare, poiché il team aggiornava continuamente la base di codice sotto lipotesi di unAPI POSIX e poiché la base di codice non veniva ancora compilata con Windows, soluzioni che erano “drop-in” e che potevano essere impiegate in modo trasparente con modifiche minime per ottenere unampia compatibilità tra lintera base di codice è stata molto più utile per evitare conflitti di unione sintattici o semantici rispetto alle soluzioni di natura chirurgica. Per questo motivo, invece di modificare ogni sito di chiamata, abbiamo creato i nostri shim di Windows equivalenti per le API POSIX che simulavano il comportamento desiderato, anche se non era ottimale complessivamente. Ciò ci ha consentito di garantire la compilabilità e di arrestare la proliferazione di modifiche incompatibili molto più rapidamente (e di affrontare successivamente in modo uniforme ogni singolo problema dellintera base di codice in batch) di quanto sarebbe possibile altrimenti.

Collegabilità

Una volta ottenuta la compilabilità, il problema successivo è stato il corretto collegamento dei file oggetto per ottenere i binari eseguibili.Sebbene teoricamente semplici, i problemi di collegabilità spesso comportavano molte complessità accidentali, come le seguenti:

  • Alcune librerie di sistema erano specifiche della piattaforma e non disponibili o diverse su altre piattaforme ( come libpthread)
  • Alcune librerie sono state collegate dinamicamente quando ci si aspettava che fossero collegate staticamente (o viceversa)
  • Alcune definizioni di simboli erano mancanti o in conflitto (ad esempio, connect da hiredis in conflitto con connect per i socket)
  • Alcune dipendenze funzionavano casualmente sui sistemi POSIX ma richiedevano una gestione esplicita in Bazel su Windows

Le soluzioni per queste erano spesso modifiche banali (anche se forse non ovvie) ai file di compilazione, sebbene in alcuni casi fosse necessario correggere le dipendenze. Come prima, la capacità di Bazel di patchare praticamente qualsiasi file da una fonte esterna è stata estremamente utile per risolvere tali problemi.

Di Richy Great

Ottenere la codebase per costruire correttamente è stato una tappa importante. Una volta integrate le modifiche specifiche della piattaforma, lintero team si assumerebbe ora la responsabilità di garantire la portabilità statica in tutte le modifiche future e lintroduzione di modifiche incompatibili sarebbe ridotta al minimo in tutta la codebase. (La portabilità dinamica ovviamente non era ancora possibile a questo punto, poiché gli stub spesso mancavano delle funzionalità necessarie su Windows e di conseguenza il codice non poteva essere eseguito in questa fase.)

Ottenere la base di codice per costruire con successo è stata una pietra miliare importante. Una volta integrate le modifiche specifiche della piattaforma, lintero team si assumerebbe ora la responsabilità di garantire la portabilità statica in tutte le modifiche future e lintroduzione di modifiche incompatibili sarebbe ridotta al minimo in tutta la codebase. (La portabilità dinamica ovviamente non era ancora possibile a questo punto, poiché gli stub spesso mancavano delle funzionalità necessarie su Windows e di conseguenza il codice non poteva essere eseguito in questa fase.)

Questo non solo ha ridotto il carico di lavoro, ma ha consentito allo sforzo di portabilità di Windows di rallentare e procedere più lentamente e con attenzione per affrontare in profondità le incompatibilità più difficili, senza la necessità di distogliere lattenzione dalle modifiche più importanti che venivano costantemente introdotte nel codice. Ciò consentiva soluzioni di qualità superiore a lungo termine, incluso un refactoring della base di codice che potenzialmente influiva sulla semantica su altre piattaforme.

Compatibilità statica (principalmente C / C ++)

Questo era forse la fase più dispendiosa in termini di tempo, in cui gli stub di compatibilità sarebbero stati rimossi o elaborati, il codice disabilitato potrebbe essere riattivato e i singoli problemi potrebbero essere risolti. La natura statica del C ++ consentiva alla diagnostica del compilatore di guidare la maggior parte di tali modifiche necessarie senza la necessità di eseguire alcun codice.

Le singole modifiche qui, comprese alcune menzionate in precedenza, sarebbero troppo lunghe per essere discusse in profondità nella loro interezza, e alcuni problemi individuali sarebbero probabilmente degni dei loro lunghi post sul blog. La generazione e la gestione del processo, ad esempio, è in realtà unattività sorprendentemente complessa piena di idiosincrasie e insidie ​​(vedere qui per esempio) per la quale sembra non esserci nulla di buono libreria multipiattaforma, nonostante questo sia un vecchio problema. In parte ciò è dovuto a progetti iniziali scadenti nelle stesse API del sistema operativo. ( Il codice sorgente di Chromium offre uno sguardo ad alcune delle complessità che spesso vengono ignorate altrove. In effetti, il codice sorgente di Chromium può spesso servire come un ottimo esempio di gestione molte incompatibilità e sottigliezze della piattaforma.) Il nostro iniziale disprezzo per questo fatto ci ha portato a tentare di utilizzare Boost.Process, ma la mancanza di una chiara semantica di proprietà per pid_t di POSIX (che è usato per entrambi identificazione del processo e proprietà) così come i bug nella libreria Boost.Process stessa hanno reso difficile trovare bug nella base di codice, determinando alla fine la nostra decisione di annullare questa modifica e introdurre la nostra astrazione. Inoltre, la libreria Boost.Process era piuttosto pesante anche per una libreria Boost, e questo ha rallentato notevolmente le nostre build. Invece, abbiamo finito per scrivere il nostro wrapper per gli oggetti di processo. Si è rivelato funzionare abbastanza bene per i nostri scopi. Uno dei nostri suggerimenti in questo caso era di considerare soluzioni su misura per le nostre esigenze e non dare per scontato che le soluzioni preesistenti fossero la scelta migliore .

Questo era, ovviamente, solo un assaggio della questione della gestione dei processi.Forse vale la pena approfondire anche alcuni altri aspetti dello sforzo di portabilità.

Di Thomas Jensen

Parti del codice base (come la comunicazione con il server Plasma in Arrow) presunte la possibilità di utilizzare socket di dominio UNIX (AF_UNIX) su Windows. Sebbene le versioni recenti di Windows 10 supportino i socket del dominio UNIX, le implementazioni non sono sufficienti per coprire tutti i possibili usi del dominio UNIX socket, né i socket di dominio UNIX sono particolarmente eleganti: richiedono la pulizia manuale e possono lasciare file non necessari sul filesystem nel caso di una terminazione non pulita di un processo, e non forniscono la possibilità di inviare dati ausiliari (come descrittori di file) in un altro processo. Poiché Ray usa Boost.Asio, il sostituto più conveniente per i socket di dominio UNIX erano i socket TCP locali (entrambi potevano essere astratti come socket generali), quindi siamo passati a questultimo, semplicemente sostituendo i percorsi dei file con versioni in stringa di indirizzi TCP / IP senza dover effettuare il refactoring della base di codice.

Ciò non era sufficiente, poiché luso di socket TCP non forniva ancora la possibilità di replicare descrittori di socket in altri processi. In effetti, una soluzione adeguata a questo problema sarebbe stata quella di evitarlo del tutto. Poiché ciò richiederebbe un refactoring a più lungo termine delle parti rilevanti del codice base (un compito che altri hanno intrapreso), tuttavia, nel frattempo è apparso più appropriato un approccio trasparente. Ciò è stato reso difficile dal fatto che, su sistemi basati su UNIX, la duplicazione di un descrittore di file non richiede la conoscenza dellidentità del processo di destinazione, mentre, su Windows, la duplicazione di un handle richiede la manipolazione attiva del processo di destinazione. Per risolvere questi problemi, abbiamo implementato la possibilità di scambiare descrittori di file sostituendo la procedura di handshake allavvio del negozio Plasma con un meccanismo più specializzato che stabilisca una connessione TCP , cerca lID del processo allaltra estremità (una procedura forse lenta, ma una tantum), duplica lhandle del socket e informa il processo di destinazione del nuovo handle. Sebbene questa non sia una soluzione generica (e in effetti potrebbe essere soggetta a condizioni di gara nel caso generale) e un approccio piuttosto insolito, ha funzionato bene per gli scopi di Ray e potrebbe essere un approccio ad altri progetti che affrontano lo stesso problema potrebbe trarne vantaggio.

Oltre a questo, uno dei maggiori problemi che ci aspettavamo di dover affrontare era la nostra dipendenza dal server Redis . Sebbene Microsoft Open Technologies (MSOpenTech) avesse precedentemente implementato un port di Redis su Windows, il progetto era stato abbandonato e di conseguenza non supportava le versioni di Redis richieste da Ray . Questo inizialmente ci ha fatto supporre che avremmo dovuto eseguire ancora il server Redis sul sottosistema Windows per Linux (WSL), il che probabilmente si sarebbe rivelato scomodo per gli utenti. Siamo stati molto grati, quindi, di scoprire che un altro sviluppatore aveva continuato il progetto per produrre successivi binari di Redis su Windows (vedere tporadowski / redis ). Questo ha semplificato enormemente il nostro problema e ci ha permesso di fornire il supporto nativo di Ray per Windows.

Infine, forse gli ostacoli più significativi che abbiamo dovuto affrontare (come ha fatto MSOpenTech Redis e come molti altri programmi solo POSIX) sono stati lassenza di banali sostituti per alcune API POSIX su Windows. Alcuni di questi ( come

getppid()) erano semplici, anche se alquanto noiosi. Forse il problema più difficile riscontrato durante lintero processo di porting, tuttavia, è stato quello dei descrittori di file rispetto agli handle di file. Gran parte del codice su cui ci siamo basati (come il negozio Plasma in Arrow) presupponeva luso di descrittori di file POSIX (int s). Tuttavia, Windows utilizza nativamente i HANDLE, che hanno dimensioni di un puntatore e sono analoghi a size_t. Di per sé, tuttavia, questo non è un problema significativo, poiché il runtime di Microsoft Visual C ++ (CRT) fornisce un livello simile a POSIX. Tuttavia, il livello ha funzionalità limitate, richiede traduzioni in ogni sito di chiamata che non lo supporta e, in particolare, non può essere utilizzato per cose come socket o handle di memoria condivisa. Inoltre, non volevamo presumere che i HANDLE sarebbero sempre stati abbastanza piccoli da stare in un numero intero a 32 bit, anche se questo era spesso il caso, poiché non era chiaro se circostanze di cui non eravamo a conoscenza potevano infrangere silenziosamente questo assunto.Ciò ha aggravato i nostri problemi in modo significativo, poiché la soluzione più ovvia sarebbe stata quella di rilevare tutti i int che rappresentano i descrittori di file in una libreria come Arrow, e di sostituirli (e tutti i loro usi ) con un tipo di dati alternativo, che era un processo soggetto a errori e comportava patch significative al codice esterno, creando un carico di manutenzione significativo.

Era abbastanza difficile decidere cosa fare in questa fase. La soluzione di MSOpenTech Redis per lo stesso problema ha chiarito che si trattava di un compito piuttosto scoraggiante, poiché avevano risolto il problema creando una tabella descrittore di file a livello di processo singleton in cima dellimplementazione CRT esistente, richiedendo loro di occuparsi della sicurezza dei thread, oltre a costringerli a intercettare tutti gli usi delle API POSIX (anche quelli che era già in grado di gestire) semplicemente per tradurre descrittori di file. Invece, abbiamo deciso di adottare un approccio insolito: abbiamo esteso il livello di traduzione POSIX nel CRT . Ciò è stato fatto identificando le maniglie incompatibili al momento della creazione e “spingendo” quelle maniglie nei respingenti di un tubo superfluo, restituendo invece il descrittore di quel tubo. Quindi dovevamo solo modificare i siti di utilizzo di questi handle, che erano, soprattutto, banali da identificare, poiché erano tutti socket e API file mappate in memoria . In effetti, questo ha aiutato a evitare la necessità di applicare patch, poiché siamo stati in grado di reindirizzare semplicemente molte funzioni tramite le macro .

Mentre lo sforzo per sviluppare questo livello di estensione unico (in win32fd.h) è stato significativo (e abbastanza poco ortodosso), probabilmente ne è valsa la pena, poiché il livello di traduzione era infatti piuttosto piccolo in confronto e ci ha permesso di delegare la maggior parte dei problemi non correlati (come il blocco multithread della tabella dei descrittori di file) alle API CRT. Inoltre, sfruttando pipe anonime dalla stessa tabella descrittore di file globale (nonostante la nostra mancanza di accesso diretto ad essa), siamo stati in grado di evitare di dover intercettare e tradurre descrittori di file per altre funzioni che potrebbero essere già gestite direttamente. Ciò ha consentito a gran parte del codice di rimanere sostanzialmente invariato con un impatto minimo sulle prestazioni, fino a quando in seguito non abbiamo avuto la possibilità di rifattorizzare il codice e fornire wrapper migliori a un livello superiore (ad esempio tramite wrapper Boost.Asio). È del tutto possibile che unestensione di questo livello possa consentire ad altri progetti come Redis di essere portati su Windows in modo molto più fluido e con modifiche molto meno drastiche o potenziali bug.

Di Andrea Leopardi

Eseguibilità run-time (Proof-of-Concept)

Una volta che abbiamo creduto che il core di Ray funzionasse correttamente, il traguardo successivo è stato quello di garantire che un test Python potesse esercitare con successo i nostri percorsi di codice. Inizialmente, non abbiamo dato la priorità a farlo. Tuttavia, questo si è rivelato un errore, poiché le modifiche successive di altri sviluppatori del team hanno infatti introdotto incompatibilità più dinamiche con Windows e il sistema CI non è stato in grado di rilevare tali rotture. Successivamente, abbiamo quindi reso prioritario eseguire un test minimo sulle build di Windows, per evitare ulteriori rotture della build.

Per il per la maggior parte, i nostri sforzi hanno avuto successo e qualsiasi bug persistente nel core di Ray si trovava in parti prevedibili della base di codice (anche se risolverli spesso richiedeva di passare attraverso il codice multi-processo, che era tuttaltro che banale). Tuttavia, cè stata almeno una sorpresa in qualche modo spiacevole lungo la strada sul lato C e una sul lato Python, entrambi i quali (tra le altre cose) ci hanno incoraggiato a leggere la documentazione del software in modo più proattivo in futuro.

Sul lato C, la nostra stretta di mano iniziale wrapper per lo scambio di handle di socket si è basata sulla sostituzione ingenua di sendmsg e recvmsg con WSASendMsg e WSARecvMsg. Queste API Windows erano gli equivalenti più vicini alle API POSIX e quindi sembravano essere una scelta ovvia. Tuttavia, al momento dellesecuzione, il codice si bloccava costantemente e lorigine del problema non era chiara. Alcuni debug (incluso con le versioni di debug dei & runtime) hanno aiutato a rivelare che il problema era con le variabili dello stack passate a WSASendMsg. Un ulteriore debug e unattenta ispezione del contenuto della memoria hanno suggerito che il problema potrebbe essere stato il msg_flags campo di WSAMSG, poiché questo era lunico non inizializzato campo.Tuttavia, questo sembrava essere irrilevante: msg_flags è stato semplicemente tradotto da flags in struct msghdr, che non è stato utilizzato in input ed è stato semplicemente utilizzato come parametro di output. La lettura della documentazione, tuttavia, ha rivelato il problema: su Windows, il campo fungeva anche da parametro input , e quindi lasciarlo non inizializzato comportava un comportamento imprevedibile! Questo è stato abbastanza inaspettato per noi e ha portato a due importanti suggerimenti: leggere attentamente la documentazione di ogni funzione e inoltre, inizializzare le variabili non serve solo a garantire la correttezza con le API attuali, ma è anche importante per rendere il codice robusto per future modifiche alle API target .

Sul lato Python, abbiamo riscontrato un problema diverso. I nostri moduli Python nativi inizialmente non riuscirebbero a caricarsi, nonostante la mancanza di problemi evidenti. Dopo diversi giorni di congetture, passaggi attraverso lassemblaggio e il codice sorgente CPython e lispezione delle variabili nel codice base CPython, è diventato evidente che il problema era la mancanza di un suffisso .pyd su Python dinamico librerie su Windows. A quanto pare, per motivi a noi poco chiari, Python si rifiuta di caricare anche i file .dll su Windows come moduli Python, nonostante il fatto che le librerie condivise native potrebbero normalmente essere caricate anche con qualsiasi file estensione. In effetti, si è scoperto che questo fatto era documentato sul sito web di Python . Purtroppo, tuttavia, la presenza di tale documentazione non può implicare la nostra consapevolezza di cercarla.

Tuttavia, alla fine, Ray è stato in grado di funzionare con successo su Windows, e questo ha concluso la tappa successiva e ha fornito una prova di concetto per limpresa.

Di Hitesh Choudhary

Compatibilità run-time (principalmente Python)

A questo punto, una volta che il nucleo di Ray era funzionando, siamo stati in grado di concentrarci sul porting del codice di livello superiore. Alcuni problemi erano piuttosto facili da risolvere: ad esempio, alcune API Python che sono solo UNIX (ad esempio, os.uname()[1]) spesso hanno sostituzioni adeguate su Windows (come socket.gethostname()) e trovarli era una questione di sapere per cercare tutte le istanze di essi nella base di codice. Altri problemi erano più difficili da rintracciare o risolvere. A volte erano dovuti alluso di comandi specifici di POSIX (come ps), che richiedevano approcci alternativi (come luso di psutil per Python). Altre volte, erano dovuti a incompatibilità nelle librerie di terze parti. Ad esempio, quando un socket si disconnette su Windows, viene generato un errore, anziché restituire letture vuote. La libreria Python per Redis non sembra gestirla. Tali differenze di comportamento richiedevano patch di scimmia per evitare occasionalmente errori di confusione che si sarebbero verificati alla chiusura di Ray.

Sebbene alcuni di questi problemi siano abbastanza noioso ma probabilmente previsto (come sostituire gli usi di /tmp con la directory temporanea della piattaforma, o evitare lipotesi che tutti i percorsi assoluti inizino con una barra), alcuni erano alquanto inaspettati (come prenotazioni di porte ) o (come spesso accade) dovute a presupposti errati, e la loro comprensione dipende dalla comprensione dellarchitettura di Windows e dei suoi approcci alla retrocompatibilità.

Una di queste storie ruota attorno alluso di barre come separatori di directory su Windows. In generale, questi sembrano funzionare bene e sono comunemente usati dagli sviluppatori. Tuttavia, ciò è in realtà dovuto alla conversione automatica delle barre in barre rovesciate nelle librerie del sottosistema di Windows in modalità utente e alcune elaborazioni automatiche possono essere soppresse aggiungendo esplicitamente ai percorsi un prefisso \\?\, che è utile per aggirare alcune funzionalità di compatibilità (come i percorsi lunghi). Tuttavia, non stavamo mai utilizzando esplicitamente tale percorso e presumevamo che ci si potesse aspettare che gli utenti evitassero un utilizzo insolito nelle nostre versioni sperimentali. Tuttavia, in seguito divenne evidente che quando Bazel invocava alcuni test Python, i percorsi sarebbero stati elaborati in questo formato per consentire lutilizzo di percorsi lunghi, e questo disabilitava la traduzione automatica su cui ci affidavamo implicitamente. Questo ci ha portato a conclusioni importanti: in primo luogo, che è generalmente meglio utilizzare le API nel modo più adatto per il sistema di destinazione, in quanto offre meno opportunità che si verifichino problemi imprevisti .Secondo, e soprattutto, è semplicemente un errore presumere che lambiente di un utente sia prevedibile . La realtà è che il software moderno si basa quasi sempre sullesecuzione di codice di terze parti di cui non siamo consapevoli dei comportamenti precisi. Anche quando si può presumere che un utente eviti situazioni problematiche, il software di terze parti è completamente ignaro di tali presupposti nascosti. Quindi è probabile che si verifichino comunque, non solo per gli utenti, ma anche per gli stessi sviluppatori di software, provocando bug più difficili da rintracciare che da risolvere durante la scrittura del codice iniziale. Quindi è importante evitare di dare troppo peso alla facilità di utilizzo del programma (lopposto di “facilità duso”) quando si progetta un sistema robusto.

(A parte il divertimento: infatti, su Windows, i percorsi può infatti contenere virgolette e molti altri caratteri speciali che normalmente si presume siano illegali. Ciò si verifica quando si utilizzano flussi di dati alternativi NTFS. Tuttavia, questi sono abbastanza rari e complessi che anche le librerie di linguaggi standard spesso non li gestiscono.)

Una volta risolti i problemi più significativi, tuttavia, molti test sono stati in grado di superare Windows, creando la prima implementazione Windows sperimentale di Ray.

Di Ross Sneddon

Miglioramenti in fase di esecuzione (ad esempio, supporto Unicode)

A questo punto, gran parte del nucleo di Ray può essere utilizzato su Windows proprio come su altre piattaforme. Tuttavia, rimangono alcuni problemi, che richiedono sforzi continui per risolverli.

Il supporto Unicode è uno di questi problemi. Per motivi storici, il sottosistema in modalità utente di Windows ha due versioni della maggior parte delle API: una versione “ANSI” che supporta set di caratteri a byte singolo e una versione “Unicode” che supporta UCS-2 o UTF-16 (a seconda del particolari dellAPI in questione). Sfortunatamente, nessuno di questi è UTF-8; anche il supporto di base per Unicode richiede luso di stringhe di caratteri larghi (basate su wchar_t) nellintera base di codice. (nb: In effetti, Microsoft ha recentemente tentato di introdurre UTF-8 come tabella codici, ma non è sufficientemente supportato per risolvere questo problema senza problemi, almeno senza fare affidamento su componenti interni di Windows potenzialmente non documentati e fragili.)

Tradizionalmente, i programmi Windows gestiscono Unicode tramite luso di macro come _T() o TEXT(), che si espandono in stretto o largo -character literals a seconda che sia stata specificata o meno una build Unicode e utilizzare TCHAR come tipi di caratteri generici. Allo stesso modo, la maggior parte delle API C ha TCHAR versioni dipendenti (come _tcslen() al posto di strlen()) per consentire la compatibilità con entrambi i tipi di codice. Tuttavia, la migrazione di una base di codice basata su UNIX a questo modello è unimpresa piuttosto complessa. Questo non è stato ancora fatto in Ray, e quindi, al momento della stesura di questo documento, Ray non supporta Unicode corretto (ad esempio) nei percorsi dei file su Windows, e lapproccio migliore per farlo potrebbe essere ancora una questione aperta.

Un altro problema di questo tipo è il meccanismo di comunicazione tra processi. Sebbene i socket TCP possano funzionare bene su Windows, non sono ottimali, poiché introducono uno strato di complessità non necessaria nella logica (come timeout, keep-alive, algoritmo di Nagle), possono causare una raggiungibilità accidentale da host non locali e possono introdurre alcune prestazioni generali. In futuro, Named Pipes potrebbe fornire una migliore sostituzione per i socket di dominio UNIX su Windows; infatti, anche su Linux, anche le pipe oi cosiddetti abstract socket di dominio UNIX potrebbero rivelarsi alternative migliori, in quanto non richiedono ingombrare e ripulire i file socket sul file system.

Infine, un altro esempio di tale problema è la compatibilità dei socket BSD, o meglio, la sua mancanza. Una eccellente risposta su StackOverflow discute alcuni dei problemi in modo approfondito, ma brevemente, mentre le API socket comuni sono derivate dallAPI socket BSD originale, piattaforme diverse implementano socket simili flag in modo diverso. In particolare, i conflitti con gli indirizzi IP o le porte TCP esistenti possono produrre comportamenti diversi tra le piattaforme. Sebbene i problemi siano difficili da dettagliare qui, il risultato finale è che ciò potrebbe rendere difficile lutilizzo simultaneo di più istanze di Ray sullo stesso host. (In effetti, poiché questo dipende dal comportamento del kernel del sistema operativo, influisce anche su WSL.) Questo è ancora un altro problema noto la cui soluzione è piuttosto complicata e non completamente risolta nel sistema corrente.

Conclusione

Il processo di porting di una base di codice come quella di Ray su Windows è stata unesperienza preziosa che evidenzia i pro ei contro di molti aspetti dello sviluppo del software e il loro impatto sulla manutenzione del codice.La descrizione precedente evidenzia solo alcuni degli ostacoli incontrati lungo il percorso. Dal processo si possono trarre molte conclusioni utili, alcune delle quali potrebbero essere preziose da condividere qui per altri progetti che sperano di raggiungere un obiettivo simile.

Innanzitutto, in alcuni casi, abbiamo effettivamente trovato in seguito le versioni successive di alcune biblioteche (come hiredis) aveva già risolto alcuni problemi che avevamo affrontato. Le soluzioni non erano sempre ovvie, poiché (ad esempio) la versione di hiredis nelle recenti versioni di Redis era in realtà una copia obsoleta di hiredis, il che ci ha portato a credere che alcuni problemi non fossero ancora stati risolti. Né le revisioni successive hanno sempre risolto completamente tutti i problemi di compatibilità esistenti. Tuttavia, sarebbe stato possibile risparmiare un po di tempo per controllare più a fondo le soluzioni esistenti ad alcuni problemi per evitare di doverli risolvere di nuovo.

Di John Barkiple

Secondo, la catena di fornitura del software è spesso complessa . I bug possono naturalmente accumularsi a ogni livello ed è un errore ritenere che anche strumenti open source ampiamente utilizzati siano “testati in battaglia” e quindi robusti, specialmente se utilizzati in rabbia . Inoltre, molti problemi di ingegneria del software di vecchia data o comuni non hanno soluzioni soddisfacenti disponibili per luso, specialmente (ma non solo) quando richiedono compatibilità tra sistemi diversi. In effetti, a parte semplici stranezze, nel processo di porting di Ray su Windows, abbiamo regolarmente riscontrato e spesso segnalato bug in numerosi software, incluso ma non limitato a un bug di Git su Linux che ha influito sullutilizzo di Bazel, Redis (Linux) , glog , psutil (bug di analisi che interessa WSL) , grpc , molti bug difficili da identificare in Bazel stesso (ad es. 1 , 2 , 3 , 4 ), Travis CI e GitHub Actions , tra le altre. Questo ci ha incoraggiato a prestare maggiore attenzione anche alla complessità delle nostre dipendenze.

Terzo, investendo in strumenti e infrastrutture paga dividendi nel lungo periodo. Le build più veloci consentono uno sviluppo più rapido e strumenti più potenti consentono di risolvere più facilmente problemi complessi. Nel nostro caso, luso di Bazel ci ha aiutato in molti modi, nonostante sia tuttaltro che perfetto e la sua imposizione di una curva di apprendimento ripida. Investire del tempo (possibilmente più giorni) per apprendere le capacità, i punti di forza e le carenze di un nuovo strumento è raramente facile, ma è probabile che sia utile nella manutenzione del codice. Nel nostro caso, dedicare del tempo alla lettura approfondita della documentazione di Bazel ci ha permesso di individuare molto più rapidamente una moltitudine di problemi e soluzioni futuri. Inoltre, ci ha anche aiutato a integrare strumenti con Bazel che pochi altri erano apparentemente riusciti a realizzare, come lo strumento include-what-you-use di Clang.

Quarto, e come accennato in precedenza, è prudente impegnarsi in pratiche di codifica sicure come inizializzare la memoria prima delluso quando non vi è alcun compromesso significativo. Anche lingegnere più attento non può necessariamente prevedere levoluzione futura del sistema sottostante che potrebbe invalidare silenziosamente i presupposti.

Infine, come generalmente accade in medicina, prevenzione è la cura migliore . Tenere conto di possibili sviluppi futuri e codificare su interfacce standardizzate consente una progettazione del codice più estensibile di quanto si possa facilmente ottenere dopo linsorgere di incompatibilità.

Sebbene il port di Ray a Windows non sia ancora completo, è stato abbastanza successo finora e speriamo che la condivisione della nostra esperienza e delle nostre soluzioni possa servire come guida utile per altri sviluppatori che stanno valutando di intraprendere un viaggio simile.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *