Portando o Ray para o Microsoft Windows

Por Johny vino

(Mehrdad Niknami) (28 de setembro de 2020)

Background

Quando Ray foi lançado pela primeira vez, ele foi escrito para Linux e macOS – sistemas operacionais baseados em UNIX. Faltava suporte para Windows, o sistema operacional de desktop mais popular, mas era importante para o sucesso a longo prazo do projeto. Embora o subsistema Windows para Linux (WSL) fornecesse uma opção possível para alguns usuários, ele limitava o suporte a versões recentes do Windows 10 e tornava muito mais difícil para o código para interagir com o sistema operacional Windows nativo, o que representava uma experiência ruim para os usuários. Diante disso, realmente esperávamos fornecer suporte nativo do Windows aos usuários, se possível.

Portar o Ray para o Windows, no entanto, estava longe de ser uma tarefa trivial. Como muitos outros projetos que tentam alcançar a portabilidade após o fato, encontramos muitos desafios cujas soluções não eram óbvias. Nesta postagem do blog, pretendemos mergulhar profundamente nos detalhes técnicos do processo que seguimos para tornar o Ray compatível com o Windows. Esperamos que isso possa ajudar outros projetos com uma visão semelhante a entender alguns dos desafios potenciais envolvidos e como eles podem ser tratados.

Visão geral

O suporte de retrofit para o Windows foi cada vez maior desafio à medida que o desenvolvimento em Ray avançava. Antes de começar, esperávamos que certos aspectos do código constituíssem uma parte significativa de nossos esforços de portabilidade, incluindo o seguinte:

  • Comunicação entre processos (IPC) e memória compartilhada
  • Identificadores de objetos e identificadores / descritores de arquivos (FDs)
  • Geração e gerenciamento de processos, incluindo sinalização
  • Gerenciamento de arquivos, gerenciamento de threads e E / S assíncrona
  • Uso do shell e do comando do sistema
  • Redis

Dada a extensão dos problemas, percebemos rapidamente que a prevenção de novas incompatibilidades deve ser priorizada em vez de tentar resolver problemas. Assim, tentamos prosseguir aproximadamente nas etapas a seguir, embora isso seja um pouco uma simplificação, já que questões específicas às vezes eram abordadas em diferentes estágios:

  1. Compilabilidade para dependências de terceiros
  2. Compilabilidade para Ray (via stubs vazios & TODOs)
  3. Linkability
  4. Integração contínua (CI) (para bloquear outras alterações incompatíveis)
  5. Compatibilidade estática (principalmente C ++)
  6. Executabilidade em tempo de execução (POC mínimo)
  7. Compatibilidade em tempo de execução (principalmente Python)
  8. Executar melhorias de tempo (por exemplo, suporte Unicode)
Por Ashkan Forouzani

Processo de desenvolvimento

Em alto nível, as abordagens aqui podem variar. Para projetos menores, pode ser altamente benéfico simplificar a base de código ao mesmo tempo que o código está sendo transferido para uma nova plataforma. No entanto, a abordagem que adotamos, que se mostrou bastante útil para uma base de código grande e que muda dinamicamente, foi resolver os problemas um de cada vez , manter as alterações ortogonais entre si quanto possível e priorizar a preservação da semântica sobre a simplicidade .

Às vezes, isso exigia a escrita de código potencialmente estranho para lidar com condições que podem não ter ocorrido necessariamente na produção (digamos, citando um caminho do arquivo que nunca teria espaços). Em outras ocasiões, fazer isso exigia a introdução de soluções “mecânicas” de uso mais geral que podem ter sido evitáveis ​​(como usar um std::shared_ptr para certos casos em que um projeto diferente pode ter foi permitido que um std::unique_ptr fosse usado). No entanto, preservar a semântica do código existente era absolutamente crucial para evitar a introdução constante de novos bugs na base de código, o que teria afetado o resto da equipe. E, em retrospectiva, essa abordagem foi bem-sucedida: bugs em outras plataformas resultantes de alterações relacionadas ao Windows eram muito raros e ocorriam com mais frequência devido a alterações nas dependências do que devido a alterações semânticas na base de código.

Compilabilidade (dependências de terceiros)

O primeiro obstáculo foi garantir que as dependências de terceiros pudessem ser construídas no Windows. Embora muitas de nossas dependências fossem bibliotecas amplamente utilizadas e a maioria não tivesse incompatibilidades maiores , isso nem sempre era simples. Em particular, as complexidades acidentais abundaram em várias facetas do problema.

  • Os arquivos de construção (em particular os arquivos de construção do Bazel) para alguns projetos às vezes eram inadequados no Windows, exigindo patches . Freqüentemente, isso ocorria devido a problemas que ocorriam mais raramente em plataformas UNIX, como o problema de citar caminhos de arquivo com espaços, de iniciar o interpretador Python adequado ou o problema de vincular corretamente bibliotecas compartilhadas versus bibliotecas estáticas. Felizmente, uma das ferramentas mais úteis para resolver esse problema foi o próprio Bazel: sua capacidade integrada de corrigir espaços de trabalho externos é, como aprendemos rapidamente, extremamente útil. Isso permitia um método padrão de patchear bibliotecas externas sem modificar os processos de construção de uma maneira ad-hoc, mantendo a base de código limpa.
  • Os conjuntos de ferramentas de construção exibiam suas próprias complexidades, já que a escolha do compilador ou do vinculador frequentemente afetava as bibliotecas, APIs e definições em que você pode confiar. E, infelizmente, trocar de compiladores no Bazel pode ser bastante complicado . No Windows, em particular, a melhor experiência geralmente vem com o uso das ferramentas de compilação do Microsoft Visual C ++, mas nossos conjuntos de ferramentas em outras plataformas eram baseados em GCC ou Clang. Felizmente, a cadeia de ferramentas do LLVM vem com o Clang-Cl no Windows, que permite usar uma combinação de recursos do Clang e do MSVC, tornando muitos problemas muito mais fáceis de resolver.
  • As dependências da biblioteca geralmente apresentam as maiores dificuldades. Às vezes, o problema era tão comum quanto a falta de um cabeçalho ou uma definição conflitante que poderia ser tratada mecanicamente, a que mesmo as bibliotecas amplamente utilizadas (como Boost) não eram imunes. As soluções para isso geralmente eram algumas definições de macro ou cabeçalhos fictícios apropriados. Em outras instâncias – como para as bibliotecas hiredis e Arrow – bibliotecas exigiam a funcionalidade POSIX que não estava disponível no Windows (como sinais ou a capacidade de passar descritores de arquivo por soquetes de domínio UNIX). Isso se mostrou muito mais difícil de resolver em alguns casos. Como estávamos mais preocupados com a compilabilidade neste estágio, entretanto, foi proveitoso adiar a implementação de APIs mais complexas para um estágio posterior e utilizar stubs básicos ou desativar o código ofensivo para permitir que a construção continue.

Assim que terminarmos de compilar as dependências, poderemos nos concentrar em compilar o núcleo do próprio Ray.

Compilabilidade (Ray)

O próximo obstáculo foi fazer o próprio Ray compilar, junto com o armazenamento Plasma (parte da Seta ). Isso era mais difícil porque o código geralmente não era projetado para o modelo do Windows e dependia muito de APIs POSIX. Em alguns casos, isso era apenas uma questão de encontrar e usar os cabeçalhos apropriados (como WinSock2.h no lugar de sys/time.h para struct timeval, o que pode ser surpreendente) ou criando substitutos ( como para

unistd.h ) Em outros casos, desabilitamos o código incompatível e deixamos TODOs para resolver no futuro.

Embora isso fosse conceitualmente semelhante ao caso de lidar com dependências de terceiros, uma preocupação única de alto nível que surgiu aqui foi minimizar as alterações na base de código , em vez de fornecer o solução mais elegante possível. Em particular, como a equipe atualizou continuamente a base de código sob a suposição de uma API POSIX e como a base de código ainda não compilou com o Windows, soluções que foram “drop-in” e que poderiam ser empregadas de forma transparente com mudanças mínimas para obter compatibilidade abrangente entre a base de código inteira foi muito mais útil para evitar conflitos de mesclagem sintática ou semântica do que soluções de natureza cirúrgica. Devido a esse fato, em vez de modificar cada site de chamada, criamos nossos próprios shims do Windows para APIs POSIX que simulavam o comportamento desejado, mesmo que isso fosse inferior ao ideal No geral. Isso nos permitiu garantir a compilabilidade e interromper a proliferação de alterações incompatíveis com muito mais rapidez (e posteriormente abordar de maneira uniforme cada problema individual em toda a base de código em lote) do que seria possível de outra forma.

Capacidade de conexão

Uma vez que a compilabilidade foi alcançada, o próximo problema foi a vinculação adequada dos arquivos objeto para obter binários executáveis.Embora teoricamente simples, os problemas de linkabilidade frequentemente envolviam muitas complexidades acidentais, como o seguinte:

  • Algumas bibliotecas do sistema eram específicas da plataforma e não estavam disponíveis ou eram diferentes em outras plataformas ( como libpthread)
  • Algumas bibliotecas foram vinculadas dinamicamente quando se esperava que estivessem vinculadas estaticamente (ou vice-versa)
  • Certas definições de símbolo estavam faltando ou eram conflitantes (por exemplo, connect de hiredis conflitante com connect para sockets)
  • Certas dependências estavam coincidentemente funcionando em sistemas POSIX, mas requeriam tratamento explícito em Bazel no Windows

As soluções para estes eram frequentemente mudanças mundanas (embora talvez não óbvias) nos arquivos de construção, embora em alguns casos, fosse necessário corrigir as dependências. Como antes, a capacidade do Bazel de corrigir praticamente qualquer arquivo de uma fonte externa foi extremamente útil para ajudar a resolver esses problemas.

Por Richy Ótimo

Obter a base de código para compilar com sucesso foi um marco importante. Uma vez que as mudanças específicas da plataforma fossem integradas, toda a equipe assumiria agora a responsabilidade de garantir a portabilidade estática em todas as mudanças futuras e a introdução de mudanças incompatíveis seria minimizada em todo o código-base. (A portabilidade dinâmica obviamente ainda não era possível neste ponto, já que os stubs muitas vezes não tinham a funcionalidade necessária no Windows e, conseqüentemente, o código não podia ser executado neste estágio.)

Fazer com que a base de código fosse construída com sucesso foi um marco importante. Uma vez que as mudanças específicas da plataforma fossem integradas, toda a equipe assumiria agora a responsabilidade de garantir a portabilidade estática em todas as mudanças futuras e a introdução de mudanças incompatíveis seria minimizada em todo o código-base. (A portabilidade dinâmica obviamente ainda não era possível neste ponto, já que os stubs muitas vezes não tinham a funcionalidade necessária no Windows e, conseqüentemente, o código não podia ser executado neste estágio.)

Isso não apenas diminuiu a carga de trabalho, mas também permitiu que o esforço de portabilidade do Windows diminuísse e continuasse mais lenta e cuidadosamente para abordar incompatibilidades mais difíceis em profundidade, sem a necessidade de desviar a atenção para alterações de interrupção constantemente introduzidas na base de código. Isso permitiu soluções de alta qualidade a longo prazo, incluindo alguma refatoração da base de código que potencialmente afetava a semântica em outras plataformas.

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

Isso foi possivelmente o estágio mais demorado, no qual os stubs de compatibilidade seriam removidos ou elaborados, o código desabilitado poderia ser reativado e problemas individuais poderiam ser resolvidos. A natureza estática do C ++ permitiu que os diagnósticos do compilador orientassem a maioria das alterações necessárias sem a necessidade de executar nenhum código.

As alterações individuais aqui, incluindo algumas mencionadas anteriormente, seriam muito longas para serem discutidas em profundidade em sua totalidade, e certos problemas individuais provavelmente seriam dignos de suas próprias longas postagens no blog. A geração e gerenciamento de processos, por exemplo, é na verdade um empreendimento surpreendentemente complexo, cheio de idiossincrasias e armadilhas (veja aqui por exemplo) para o qual parece não ser bom, biblioteca de plataforma cruzada, apesar de ser um problema antigo. Parte disso se deve a projetos iniciais deficientes nas próprias APIs do sistema operacional. ( O código-fonte do Chromium fornece um vislumbre de algumas das complexidades que muitas vezes são desconsideradas em outros lugares. Na verdade, o código-fonte do Chromium pode muitas vezes servir como uma ótima ilustração de manipulação muitas incompatibilidades e sutilezas de plataforma.) Nosso desprezo inicial por esse fato nos levou a tentar usar Boost.Process, mas a falta de uma semântica de propriedade clara para pid_t do POSIX (que é usado para ambos identificação e propriedade do processo), bem como bugs na própria biblioteca Boost.Process resultou na dificuldade de encontrar bugs na base de código, resultando em nossa decisão de reverter essa alteração e introduzir nossa própria abstração. Além disso, a biblioteca Boost.Process era bastante pesada, mesmo para uma biblioteca Boost, e isso tornava nossas construções consideravelmente lentas. Em vez disso, acabamos escrevendo nosso próprio wrapper para objetos de processo. Isso acabou funcionando muito bem para nossos propósitos. Uma das nossas lições neste caso foi considerar soluções sob medida para nossas próprias necessidades e não assumir que soluções preexistentes são a melhor escolha .

Isso, é claro, foi apenas um vislumbre da questão do gerenciamento de processos.Talvez valha a pena elaborar alguns outros aspectos do esforço de portabilidade também.

Por Thomas Jensen

Partes da base de código (como a comunicação com o servidor de Plasma em Arrow) assumidas a capacidade de usar soquetes de domínio UNIX (AF_UNIX) no Windows. Embora as versões recentes do Windows 10 forneçam suporte a soquetes de domínio UNIX, as implementações não são suficientes para cobrir todos os usos possíveis de domínio UNIX soquetes, nem soquetes de domínio UNIX são particularmente elegantes: eles exigem limpeza manual e podem deixar arquivos desnecessários no sistema de arquivos no caso de um encerramento não limpo de um processo e não fornecem a capacidade de enviar dados auxiliares (como descritores de arquivo) para outro processo. Como Ray usa Boost.Asio, a substituição mais conveniente para os soquetes de domínio UNIX eram os soquetes TCP locais (ambos poderiam ser abstraídos como soquetes gerais), então mudamos para o último, apenas substituir os caminhos dos arquivos por versões em string de endereços TCP / IP sem a necessidade de refatorar a base de código.

Isso não era suficiente, pois o uso de soquetes TCP ainda não fornecia a capacidade de replicação descritores de soquete em outros processos. Na verdade, uma solução adequada para esse problema teria sido evitá-lo completamente. Como isso exigiria uma refatoração de longo prazo das partes relevantes da base de código (uma tarefa que outros empreenderam), entretanto, uma abordagem transparente pareceu mais apropriada nesse ínterim. Isso foi dificultado pelo fato de que, em sistemas baseados em UNIX, a duplicação de um descritor de arquivo não requer conhecimento da identidade do processo de destino, enquanto que, no Windows, duplicar um identificador requer a manipulação ativa do processo de destino. Para resolver esses problemas, implementamos a capacidade de trocar descritores de arquivo, substituindo o procedimento de handshake ao iniciar o armazenamento de Plasma por um mecanismo mais especializado que estabeleceria uma conexão TCP , procure o ID do processo na outra extremidade (um procedimento talvez lento, mas único), duplique o identificador de soquete e informe o processo de destino do novo identificador. Embora esta não seja uma solução de propósito geral (e na verdade pode estar sujeita a condições de corrida no caso geral) e uma abordagem bastante incomum, funcionou bem para os propósitos de Ray e pode ser uma abordagem de outros projetos que enfrentam o mesmo problema poderia se beneficiar.

Além disso, um dos maiores problemas que esperávamos enfrentar era nossa dependência do servidor Redis . Embora a Microsoft Open Technologies (MSOpenTech) tenha implementado anteriormente uma porta Redis para o Windows, o projeto foi abandonado e, consequentemente, não era compatível com as versões do Redis exigidas por Ray . Isso inicialmente nos fez supor que ainda precisaríamos executar o servidor Redis no Windows Subsystem for Linux (WSL), o que provavelmente seria inconveniente para os usuários. Ficamos muito gratos, então, ao descobrir que outro desenvolvedor havia continuado o projeto para produzir binários Redis posteriores no Windows (consulte tporadowski / redis ). Isso simplificou enormemente nosso problema e nos permitiu fornecer suporte nativo de Ray para Windows.

Finalmente, possivelmente os obstáculos mais significativos que enfrentamos (assim como o MSOpenTech Redis e a maioria dos outros programas somente POSIX) foram a ausência de substitutos triviais para algumas APIs POSIX no Windows. Alguns deles ( como

getppid()) eram diretos, embora um tanto entediantes. Possivelmente, o problema mais difícil encontrado durante todo o processo de portabilidade, entretanto, foi o dos descritores de arquivo versus identificadores de arquivo. Muito do código em que confiamos (como o armazenamento de Plasma em Arrow) pressupõe o uso de descritores de arquivo POSIX (int s). No entanto, o Windows usa nativamente HANDLE s, que têm o tamanho de um ponteiro e são análogos a size_t. Por si só, no entanto, esse não é um problema significativo, pois o tempo de execução do Microsoft Visual C ++ (CRT) fornece uma camada semelhante ao POSIX. No entanto, a camada é limitada em funcionalidade, requer traduções em cada site de chamada que não a suporta e, em particular, não pode ser usada para coisas como soquetes ou identificadores de memória compartilhada. Além disso, não queríamos presumir que HANDLE s seriam sempre pequenos o suficiente para caber em um número inteiro de 32 bits, embora esse fosse o caso, pois não estava claro se circunstâncias das quais não estávamos cientes poderiam quebrar essa suposição silenciosamente.Isso agravou nossos problemas significativamente, já que a solução mais óbvia seria detectar todos os int s que representam os descritores de arquivo em uma biblioteca como Arrow, e substituí-los (e todos os seus usos ) com um tipo de dados alternativo, que era um processo sujeito a erros e envolvia patches significativos para o código externo, criando uma carga de manutenção significativa.

Foi muito difícil decidir o que fazer neste estágio. A solução do MSOpenTech Redis para o mesmo problema deixou claro que essa era uma tarefa bastante difícil, pois eles o haviam resolvido criar um singleton, tabela de descritor de arquivo de todo o processo no topo da implementação CRT existente, exigindo que eles lidem com segurança de thread, bem como forçando-os a interceptar todos os usos de APIs POSIX (mesmo aqueles que já eram capazes de manipular) apenas para traduzir descritores de arquivo. Em vez disso, decidimos adotar uma abordagem incomum: estendemos a camada de tradução POSIX no CRT . Isso foi feito identificando alças incompatíveis no momento da criação e “empurrando” essas alças nos buffers de um tubo supérfluo, retornando o descritor daquele tubo. Em seguida, apenas tivemos que modificar os locais de uso desses identificadores, que eram, crucialmente, triviais de identificar, pois eram todos socket e arquivo mapeado em memória . Na verdade, isso ajudou a evitar a necessidade de patch, já que fomos capazes de simplesmente redirecionar muitas funções por meio de macros .

Enquanto o esforço para desenvolver esta camada de extensão exclusiva (em win32fd.h) foi significativo (e bastante heterodoxo), provavelmente valeu a pena, como a camada de tradução era de fato muito pequeno em comparação e nos permitiu delegar a maioria dos problemas não relacionados (como bloqueio multithread da tabela do descritor de arquivo) às APIs CRT. Além disso, aproveitando os canais anônimos da mesma tabela descritor de arquivo global (apesar de nossa falta de acesso direto a ela), conseguimos evitar a interceptação e a tradução descritores de arquivo para outras funções que já podem ser tratadas diretamente. Isso permitiu que grande parte do código permanecesse essencialmente inalterado com um impacto mínimo no desempenho, até que mais tarde tivéssemos a chance de refatorar o código e fornecer melhores wrappers em um nível superior (como por meio de Boost.Asio wrappers). É bem possível que uma extensão dessa camada permita que outros projetos, como o Redis, sejam portados para o Windows de maneira muito mais perfeita e com mudanças muito menos drásticas ou com potencial para bugs.

Por Andrea Leopardi

Executabilidade em tempo de execução (prova de conceito)

Uma vez que acreditamos que o núcleo do Ray estava funcionando corretamente, o próximo marco foi garantir que um teste Python pudesse exercer com sucesso nossos caminhos de código. Inicialmente, não priorizamos fazer isso. No entanto, isso provou ser um erro, já que alterações posteriores feitas por outros desenvolvedores da equipe na verdade introduziram incompatibilidades mais dinâmicas com o Windows, e o sistema de CI não foi capaz de detectar tais quebras. Portanto, subsequentemente, tornamos uma prioridade executar um teste mínimo nas compilações do Windows, para evitar quebras posteriores da compilação.

Para o na maior parte, nossos esforços foram bem-sucedidos e quaisquer bugs remanescentes no núcleo do Ray estavam em partes previsíveis da base de código (embora resolvê-los muitas vezes exigisse passar pelo código de vários processos, o que estava longe de ser trivial). No entanto, houve pelo menos uma surpresa um tanto desagradável ao longo do caminho no lado C e outra no lado Python, os quais (entre outras coisas) nos encorajaram a ler a documentação do software de forma mais proativa no futuro.

No lado C, nosso handshake inicial wrapper para a troca de alças de soquete baseava-se ingenuamente na substituição de sendmsg e recvmsg com WSASendMsg e WSARecvMsg. Essas APIs do Windows eram os equivalentes mais próximos das APIs POSIX e, portanto, pareciam ser uma escolha óbvia. No entanto, durante a execução, o código travava constantemente e a origem do problema não era clara. Algumas depurações (incluindo versões de depuração de compilações & tempos de execução) ajudaram a revelar que o problema estava nas variáveis ​​da pilha passadas para WSASendMsg. Uma depuração adicional e uma inspeção detalhada do conteúdo da memória sugeriram que o problema pode ter sido o msg_flags campo de WSAMSG, pois este era o único não inicializado campo.No entanto, isso parecia ser irrelevante: msg_flags foi apenas traduzido de flags em struct msghdr, que não foi usado na entrada e foi apenas usado como um parâmetro de saída. A leitura da documentação, entretanto, revelou o problema: no Windows, o campo também servia como um parâmetro de entrada e, portanto, deixá-lo não inicializado resultava em um comportamento imprevisível! Isso foi bastante inesperado para nós e resultou em duas conclusões importantes no futuro: ler a documentação de cada função com atenção e além disso, que inicializar variáveis ​​não é apenas para garantir a exatidão com as APIs atuais, mas também é importante para tornar o código robusto para futuras alterações nas APIs .

No lado do Python, encontramos um problema diferente. Nossos módulos Python nativos inicialmente falhariam ao carregar, apesar da falta de problemas óbvios. Após vários dias de adivinhação, passando pela montagem e pelo código-fonte CPython e inspecionando variáveis ​​na base de código CPython, tornou-se aparente que o problema era a falta de um sufixo .pyd no Python dinâmico bibliotecas no Windows. Acontece que, por razões que não estão claras para nós, Python se recusa a carregar até mesmo .dll arquivos no Windows como módulos Python, apesar do fato de que bibliotecas compartilhadas nativas podem normalmente ser carregadas mesmo com qualquer arquivo extensão. Na verdade, descobriu-se que esse fato foi documentado no site do Python . Infelizmente, no entanto, a presença de tal documentação não poderia implicar em nossa compreensão de procurá-la.

No entanto, eventualmente, Ray foi capaz de rodar com sucesso no Windows, e isso concluiu o próximo marco e forneceu uma prova do conceito para o esforço.

Por Hitesh Choudhary

Compatibilidade de tempo de execução (principalmente Python)

Neste ponto, uma vez que o núcleo de Ray era funcionando, pudemos nos concentrar em portar código de nível superior. Alguns problemas eram bastante fáceis de resolver – por exemplo, algumas APIs Python que são somente UNIX (por exemplo, os.uname()[1]) geralmente têm substituições adequadas no Windows (como socket.gethostname()), e encontrá-los era uma questão de saber pesquisar todas as instâncias deles na base de código. Outros problemas eram mais difíceis de rastrear ou resolver. Às vezes, eram devido ao uso de comandos específicos de POSIX (como ps), que exigiam abordagens alternativas (como o uso de psutil para Python). Outras vezes, eram devido a incompatibilidades em bibliotecas de terceiros. Por exemplo, quando um soquete é desconectado no Windows, um erro é gerado, em vez de resultar em leituras vazias. A biblioteca Python para Redis não parecia lidar com isso. Essas diferenças de comportamento exigiam monkey patching explícito para evitar erros ocasionalmente confusos que poderiam ocorrer após o término do Ray.

Embora alguns desses problemas sejam bastante entediante, mas provavelmente esperado (como substituir os usos de /tmp pelo diretório temporário da plataforma ou evitar a suposição de que todos os caminhos absolutos começam com uma barra), alguns foram um tanto inesperados (como reservas de porta conflitantes) ou (como costuma ser o caso) devido a suposições equivocadas, e compreendê-las depende de entender a arquitetura do Windows e suas abordagens para compatibilidade com versões anteriores.

Uma dessas histórias gira em torno do uso de barras como separadores de diretório no Windows. Em geral, eles parecem funcionar bem e são comumente usados ​​por desenvolvedores. No entanto, isso é de fato devido à conversão automática de barras em barras invertidas nas bibliotecas do subsistema do Windows em modo de usuário, e certo processamento automático pode ser suprimido prefixando explicitamente os caminhos com um prefixo \\?\, que é útil para contornar certos recursos de compatibilidade (como caminhos longos). No entanto, nunca usamos explicitamente esse caminho e presumimos que os usuários deveriam evitar o uso incomum em nossas versões experimentais. No entanto, mais tarde ficou claro que quando o Bazel invocou certos testes Python, os caminhos seriam processados ​​neste formato para permitir o uso de caminhos longos, e isso desativou a tradução automática na qual confiamos implicitamente. Isso nos levou a conclusões importantes: primeiro, que geralmente é melhor usar APIs da maneira mais adequada para o sistema de destino, pois oferece menos oportunidades para que problemas inesperados ocorram .Em segundo lugar, e mais importante, é simplesmente uma falácia presumir que o ambiente de um usuário é previsível . A realidade é que o software moderno quase sempre depende da execução de código de terceiros cujos comportamentos específicos desconhecemos. Mesmo quando se pode presumir que um usuário evita situações problemáticas, o software de terceiros não tem conhecimento dessas suposições ocultas. Portanto, é provável que ocorram de qualquer maneira, não apenas para os usuários, mas também para os próprios desenvolvedores de software, resultando em bugs que são mais difíceis de rastrear do que de corrigir ao escrever o código inicial. Portanto, é importante evitar colocar muito peso na facilidade de uso do programa (o oposto de “facilidade de uso”) ao projetar um sistema robusto.

(Como diversão à parte: na verdade, no Windows, caminhos pode na verdade conter aspas e muitos outros caracteres especiais que normalmente são considerados ilegais. Isso ocorre ao usar fluxos de dados alternativos NTFS. No entanto, são raros e complexos o suficiente para que nem mesmo as bibliotecas de linguagem padrão os tratem.)

Depois que os problemas mais significativos foram resolvidos, no entanto, muitos testes foram aprovados no Windows, criando a primeira implementação experimental do Windows do Ray.

Por Ross Sneddon

Melhorias no tempo de execução (por exemplo, suporte a Unicode)

Neste ponto, muito do núcleo do Ray pode ser usado no Windows assim como em outras plataformas. No entanto, alguns problemas permanecem, o que requer esforços contínuos para resolvê-los.

O suporte Unicode é um desses problemas. Devido a razões históricas, o subsistema de modo de usuário do Windows tem duas versões da maioria das APIs: uma versão “ANSI” que suporta conjuntos de caracteres de byte único e uma versão “Unicode” que suporta UCS-2 ou UTF-16 (dependendo do detalhes da API em questão). Infelizmente, nenhum deles é UTF-8; até mesmo o suporte básico para Unicode requer o uso de strings de caracteres largos (com base em wchar_t) em toda a base de código. (Nota: Na verdade, a Microsoft recentemente tentou introduzir UTF-8 como uma página de código, mas não é bem suportado o suficiente para resolver esse problema perfeitamente, pelo menos sem depender de componentes internos do Windows potencialmente não documentados e frágeis.)

Tradicionalmente, os programas do Windows manipulam Unicode por meio do uso de macros como _T() ou TEXT(), que se expandem para estreito ou largo literais de caracteres dependendo se uma construção Unicode é especificada e usam TCHAR como seus tipos de caracteres genéricos. Da mesma forma, a maioria das APIs C tem TCHAR versões dependentes (como _tcslen() no lugar de strlen()) para permitir compatibilidade com os dois tipos de código. No entanto, a migração de uma base de código baseada em UNIX para este modelo é um esforço bastante complicado. Isso ainda não foi feito no Ray e, portanto, no momento em que este artigo foi escrito, Ray não oferece suporte a Unicode adequado em (por exemplo) caminhos de arquivo no Windows, e a melhor abordagem para fazer isso ainda pode ser uma questão em aberto.

Outro problema é o mecanismo de comunicação entre processos. Embora os soquetes TCP possam funcionar bem no Windows, eles não são ideais, pois introduzem uma camada de complexidade desnecessária na lógica (como tempos limite, keep-alives, algoritmo de Nagle), podem resultar em acessibilidade acidental de hosts não locais e podem introduzir alguma sobrecarga de desempenho. No futuro, os Pipes nomeados podem fornecer um substituto melhor para os soquetes de domínio UNIX no Windows; na verdade, mesmo no Linux, os canais ou os chamados abstratos soquetes de domínio UNIX podem provar ser alternativas melhores também, já que eles não exigem desordem e limpeza de arquivos de soquete no sistema de arquivos.

Finalmente, outro exemplo de tal problema é a compatibilidade de soquetes BSD, ou melhor, a falta dela. Uma excelente resposta no StackOverflow discute alguns dos problemas em profundidade, mas resumidamente, enquanto APIs de soquete comuns são derivados da API de soquete BSD original, diferentes plataformas implementam soquetes semelhantes sinaliza de forma diferente. Em particular, os conflitos com endereços IP ou portas TCP existentes podem produzir comportamentos diferentes nas plataformas. Embora os problemas sejam difíceis de detalhar aqui, o resultado final é que isso pode dificultar o uso de várias instâncias de Ray no mesmo host simultaneamente. (Na verdade, como isso depende do comportamento do kernel do sistema operacional, também afeta o WSL.) Este é outro problema conhecido cuja solução está bastante envolvida e não completamente abordada no sistema atual.

Conclusão

O processo de portar uma base de código como a do Ray para o Windows foi uma experiência valiosa que destaca os prós e os contras de muitos aspectos do desenvolvimento de software e seu impacto na manutenção do código.A descrição anterior destaca apenas alguns dos obstáculos encontrados ao longo do caminho. Muitas conclusões úteis podem ser tiradas do processo, algumas das quais podem ser valiosas para compartilhar aqui para outros projetos na esperança de atingir um objetivo semelhante.

Primeiro, em alguns casos, descobrimos mais tarde que versões subsequentes de algumas bibliotecas (como a hiredis) já havia resolvido alguns problemas que abordamos. As soluções nem sempre foram óbvias, pois (por exemplo) a versão do hiredis dentro das versões recentes do Redis era na verdade uma cópia desatualizada do hiredis, o que nos leva a crer que alguns problemas ainda não foram resolvidos. Nem as revisões posteriores sempre abordaram totalmente todos os problemas de compatibilidade existentes. No entanto, possivelmente teria economizado um pouco de esforço verificar mais profundamente as soluções existentes para alguns problemas para evitar ter que resolvê-los novamente.

Por John Barkiple

Segundo, a cadeia de suprimentos de software costuma ser complexa . Bugs podem se compor naturalmente em cada camada, e é uma falácia confiar que mesmo as ferramentas de código aberto amplamente utilizadas sejam “testadas em batalha” e, portanto, robustas, especialmente quando usadas em raiva . Além disso, muitos problemas de engenharia de software comuns ou antigos não têm soluções satisfatórias disponíveis para uso, especialmente (mas não apenas) quando eles exigem compatibilidade entre sistemas diferentes. Na verdade, além de meras peculiaridades, no processo de portar Ray para o Windows, regularmente encontramos e frequentemente relatamos bugs em vários softwares, incluindo, mas não se limitando a um bug do Git no Linux que afetou o uso do Bazel, Redis (Linux) , glog , psutil (bug de análise que afeta WSL) , grpc , muitos bugs difíceis de identificar no próprio Bazel (por exemplo, 1 , 2 , 3 , 4 ), Travis CI e Ações do GitHub , entre outros. Isso nos incentivou a prestar mais atenção às complexidades de nossas dependências também.

Terceiro, investindo em ferramentas e infraestrutura paga dividendos no longo prazo. Compilações mais rápidas permitem um desenvolvimento mais rápido e ferramentas mais poderosas permitem resolver problemas complexos com mais facilidade. No nosso caso, o uso do Bazel nos ajudou de muitas maneiras, apesar de estar longe de ser perfeito e de impor uma curva de aprendizado acentuada. Investir algum tempo (possivelmente vários dias) para aprender os recursos, pontos fortes e deficiências de uma nova ferramenta raramente é fácil, mas provavelmente será benéfico na manutenção do código. Em nosso caso, passar algum tempo lendo a documentação do Bazel em profundidade nos permitiu localizar com muito mais rapidez uma infinidade de problemas e soluções futuras. Além disso, também nos ajudou a integrar ferramentas com o Bazel que poucos outros aparentemente conseguiram, como a ferramenta incluir o que usar do Clang.

Quarto, e conforme mencionado anteriormente, é prudente se envolver em práticas de codificação seguras como inicializar a memória antes do uso quando não há compensação significativa. Mesmo o engenheiro mais cuidadoso não pode necessariamente prever a evolução futura do sistema subjacente que pode invalidar suposições silenciosamente.

Finalmente, como é geralmente o caso na medicina, prevenção é a melhor cura . Levar em conta possíveis desenvolvimentos futuros e codificar para interfaces padronizadas permite um design de código mais extensível do que pode ser facilmente alcançado após o surgimento de incompatibilidades.

Embora a porta de Ray para Windows ainda não esteja completa, tem sido bastante bem-sucedido até agora e esperamos que o compartilhamento de nossa experiência e soluções possa servir como um guia útil para outros desenvolvedores que estão pensando em embarcar em uma jornada semelhante.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *